mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-11 17:32:28 +00:00
Compare commits
1 Commits
coderabbit
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78edd20466 |
@@ -1,12 +0,0 @@
|
||||
reviews:
|
||||
auto_review:
|
||||
ignore_title_keywords:
|
||||
- "sync translations"
|
||||
- "update POT file"
|
||||
- "style: "
|
||||
review_status: false
|
||||
poem: false
|
||||
collapse_walkthrough: true
|
||||
sequence_diagrams: false
|
||||
changed_files_summary: false
|
||||
high_level_summary: false
|
||||
@@ -42,6 +42,3 @@ a308792ee7fda18a681e9181f4fd00b36385bc23
|
||||
# noisy typing refactoring of get_item_details
|
||||
7b7211ac79c248a79ba8a999ff34e734d874c0ae
|
||||
d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
|
||||
|
||||
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
|
||||
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
|
||||
|
||||
### Reply and Closing Policy
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -9,7 +9,7 @@ body:
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
|
||||
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
|
||||
2. When making a bug report, make sure you provide all required information. The easier it is for
|
||||
maintainers to reproduce, the faster it'll be fixed.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forum
|
||||
url: https://discuss.frappe.io/c/erpnext/6
|
||||
url: https://discuss.erpnext.com/
|
||||
about: For general QnA, discussions and community help.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ assignees: ''
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
|
||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||
the original discussion.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
|
||||
|
||||
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.frappe.io/c/framework/5
|
||||
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
|
||||
|
||||
-->
|
||||
|
||||
4
.github/helper/install.sh
vendored
4
.github/helper/install.sh
vendored
@@ -6,7 +6,7 @@ cd ~ || exit
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
|
||||
sudo apt install libcups2-dev redis-server mariadb-client
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
@@ -66,7 +66,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app payments --branch develop
|
||||
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
||||
1
.github/helper/site_config_mariadb.json
vendored
1
.github/helper/site_config_mariadb.json
vendored
@@ -8,7 +8,6 @@
|
||||
"mail_login": "test@example.com",
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"use_mysqlclient": 1,
|
||||
"root_login": "root",
|
||||
"root_password": "root",
|
||||
"host_name": "http://test_site:8000",
|
||||
|
||||
4
.github/release.yml
vendored
4
.github/release.yml
vendored
@@ -1,4 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
3
.github/workflows/backport.yml
vendored
3
.github/workflows/backport.yml
vendored
@@ -5,9 +5,6 @@ on:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -2,10 +2,6 @@ name: Trigger Docker build on release
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
curl:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.github/workflows/docs-checker.yml
vendored
3
.github/workflows/docs-checker.yml
vendored
@@ -3,9 +3,6 @@ on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, edited ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/initiate_release.yml
vendored
4
.github/workflows/initiate_release.yml
vendored
@@ -2,10 +2,6 @@
|
||||
# To add/remove versions just modify the matrix.
|
||||
|
||||
name: Create weekly release pull requests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 9:30 UTC => 3 PM IST Tuesday
|
||||
|
||||
30
.github/workflows/label-base-on-title.yml
vendored
30
.github/workflows/label-base-on-title.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: "Auto-label PRs based on title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-label-if-prefix-matches:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title and add label if it matches prefixes
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title.toLowerCase();
|
||||
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||
|
||||
// Check if the PR title starts with any of the prefixes
|
||||
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['skip-release-notes']
|
||||
});
|
||||
}
|
||||
4
.github/workflows/labeller.yml
vendored
4
.github/workflows/labeller.yml
vendored
@@ -3,10 +3,6 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
@@ -3,9 +3,6 @@ name: Linters
|
||||
on:
|
||||
pull_request: { }
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
linters:
|
||||
|
||||
10
.github/workflows/patch.yml
vendored
10
.github/workflows/patch.yml
vendored
@@ -8,14 +8,8 @@ on:
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
@@ -42,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -85,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
6
.github/workflows/patch_faux.yml
vendored
6
.github/workflows/patch_faux.yml
vendored
@@ -10,12 +10,6 @@ on:
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -3,10 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
||||
5
.github/workflows/run-indinvidual-tests.yml
vendored
5
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -7,9 +7,6 @@ concurrency:
|
||||
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -111,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -9,12 +9,6 @@ on:
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
15
.github/workflows/server-tests-mariadb.yml
vendored
15
.github/workflows/server-tests-mariadb.yml
vendored
@@ -9,9 +9,6 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
@@ -28,9 +25,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
@@ -71,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -109,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
@@ -128,9 +122,10 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
||||
env:
|
||||
TYPE: server
|
||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
|
||||
- name: Show bench output
|
||||
@@ -139,6 +134,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
@@ -147,6 +143,7 @@ jobs:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
|
||||
10
.github/workflows/server-tests-postgres.yml
vendored
10
.github/workflows/server-tests-postgres.yml
vendored
@@ -6,18 +6,12 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
types: [opened, labelled, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
|
||||
@@ -56,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -94,7 +88,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
53
.mergify.yml
53
.mergify.yml
@@ -2,27 +2,29 @@ pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=frappe-pr-bot
|
||||
- author!=mergify[bot]
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
- base=version-16
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=frappe-pr-bot
|
||||
- author!=mergify[bot]
|
||||
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
- base=version-16
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{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
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
@@ -32,6 +34,7 @@ pull_request_rules:
|
||||
- develop
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-14-hotfix
|
||||
conditions:
|
||||
- label="backport version-14-hotfix"
|
||||
@@ -41,6 +44,7 @@ pull_request_rules:
|
||||
- version-14-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-15-hotfix
|
||||
conditions:
|
||||
- label="backport version-15-hotfix"
|
||||
@@ -50,15 +54,18 @@ pull_request_rules:
|
||||
- version-15-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: backport to version-16-beta
|
||||
|
||||
- name: backport to version-13-hotfix
|
||||
conditions:
|
||||
- label="backport version-16-beta"
|
||||
- label="backport version-13-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-16-beta
|
||||
- version-13-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
@@ -89,6 +96,6 @@ pull_request_rules:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
{{ body }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [pre-commit]
|
||||
default_stages: [commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ repos:
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
|
||||
13
CODEOWNERS
13
CODEOWNERS
@@ -8,16 +8,17 @@ erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/subcontracting @mihir-kandoi
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
|
||||
.github/ @ruthra-kumar
|
||||
pyproject.toml @ruthra-kumar
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
27
README.md
27
README.md
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://frappe.io/erpnext">
|
||||
<a href="https://erpnext.com">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
</a>
|
||||
<h2>ERPNext</h2>
|
||||
@@ -7,7 +7,6 @@
|
||||
<p>Powerful, Intuitive and Open-Source ERP</p>
|
||||
</p>
|
||||
|
||||
[](https://frappe.school)<br><br>
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
||||
@@ -18,11 +17,11 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://erpnext-demo.frappe.cloud/api/method/erpnext_demo.erpnext_demo.auth.login_demo">Live Demo</a>
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
||||
-
|
||||
<a href="https://frappe.io/erpnext">Website</a>
|
||||
<a href="https://erpnext.com">Website</a>
|
||||
-
|
||||
<a href="https://docs.frappe.io/erpnext/">Documentation</a>
|
||||
<a href="https://docs.erpnext.com">Documentation</a>
|
||||
</div>
|
||||
|
||||
## ERPNext
|
||||
@@ -115,25 +114,28 @@ To setup the repository locally follow the steps mentioned below:
|
||||
2. In a separate terminal window, run the following commands:
|
||||
```
|
||||
# Create a new site
|
||||
bench new-site erpnext.localhost
|
||||
bench new-site erpnext.dev
|
||||
|
||||
# Map your site to localhost
|
||||
bench --site erpnext.dev add-to-hosts
|
||||
```
|
||||
|
||||
|
||||
3. Get the ERPNext app and install it
|
||||
```
|
||||
# Get the ERPNext app
|
||||
bench get-app https://github.com/frappe/erpnext
|
||||
|
||||
|
||||
# Install the app
|
||||
bench --site erpnext.localhost install-app erpnext
|
||||
bench --site erpnext.dev install-app erpnext
|
||||
```
|
||||
|
||||
4. Open the URL `http://erpnext.localhost:8000/app` in your browser, you should see the app running
|
||||
4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
|
||||
|
||||
## Learning and community
|
||||
|
||||
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the 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.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
|
||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||
|
||||
|
||||
@@ -142,7 +144,6 @@ To setup the repository locally follow the steps mentioned below:
|
||||
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)
|
||||
2. [Translations](https://crowdin.com/project/frappe)
|
||||
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
@@ -4,11 +4,7 @@ files:
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
- skip-release-notes
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-BR: pt_BR
|
||||
|
||||
@@ -57,7 +57,7 @@ def get_company_currency(company):
|
||||
|
||||
def set_perpetual_inventory(enable=1, company=None):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.in_test else get_default_company()
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
|
||||
company = frappe.get_doc("Company", company)
|
||||
company.enable_perpetual_inventory = enable
|
||||
@@ -77,7 +77,7 @@ def encode_company_abbr(name, company=None, abbr=None):
|
||||
|
||||
def is_perpetual_inventory_enabled(company):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.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"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
@@ -10,10 +10,8 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_company_address()
|
||||
|
||||
if hasattr(super(), "validate"):
|
||||
super().validate()
|
||||
self.update_compnay_address()
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
@@ -22,7 +20,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super().link_address()
|
||||
|
||||
def update_company_address(self):
|
||||
def update_compnay_address(self):
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
@@ -40,10 +38,6 @@ class ERPNextAddress(Address):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
|
||||
if hasattr(super(), "on_update"):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
|
||||
@@ -7,7 +7,6 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
@@ -47,8 +46,7 @@ def validate_service_stop_date(doc):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date
|
||||
and getdate(item.service_stop_date) != getdate(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))
|
||||
|
||||
@@ -319,7 +317,7 @@ def get_already_booked_amount(doc, item):
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
@@ -528,7 +526,7 @@ def make_gl_entries(
|
||||
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
raise e
|
||||
else:
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"account_currency",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_category",
|
||||
"account_type",
|
||||
"tax_rate",
|
||||
"freeze_account",
|
||||
@@ -190,20 +189,13 @@
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable"
|
||||
},
|
||||
{
|
||||
"description": "Used with Financial Report Template",
|
||||
"fieldname": "account_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Category",
|
||||
"options": "Account Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-02 06:26:44.657146",
|
||||
"modified": "2025-01-22 10:40:35.766017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -258,7 +250,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_number",
|
||||
"show_name_in_global_search": 1,
|
||||
"show_preview_popup": 1,
|
||||
@@ -266,4 +257,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -31,7 +31,6 @@ class Account(NestedSet):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
account_name: DF.Data
|
||||
account_number: DF.Data | None
|
||||
@@ -93,10 +92,8 @@ class Account(NestedSet):
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
|
||||
frozen_accounts_modifier = frappe.db.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -111,7 +108,6 @@ class Account(NestedSet):
|
||||
self.validate_parent_child_account_type()
|
||||
self.validate_root_details()
|
||||
self.validate_account_number()
|
||||
self.validate_disabled()
|
||||
self.validate_group_or_ledger()
|
||||
self.set_root_and_report_type()
|
||||
self.validate_mandatory()
|
||||
@@ -171,7 +167,7 @@ class Account(NestedSet):
|
||||
if par.root_type:
|
||||
self.root_type = par.root_type
|
||||
|
||||
if cint(self.is_group):
|
||||
if self.is_group:
|
||||
db_value = self.get_doc_before_save()
|
||||
if db_value:
|
||||
if self.report_type != db_value.report_type:
|
||||
@@ -214,7 +210,7 @@ class Account(NestedSet):
|
||||
if doc_before_save and not doc_before_save.parent_account:
|
||||
throw(_("Root cannot be edited."), RootNotEditable)
|
||||
|
||||
if not self.parent_account and not cint(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)))
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
@@ -256,14 +252,6 @@ class Account(NestedSet):
|
||||
|
||||
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
|
||||
|
||||
def validate_disabled(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.disabled) == cint(self.disabled):
|
||||
return
|
||||
|
||||
if cint(self.disabled):
|
||||
self.validate_default_accounts_in_company()
|
||||
|
||||
def validate_group_or_ledger(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
|
||||
@@ -271,44 +259,21 @@ class Account(NestedSet):
|
||||
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction cannot be converted to ledger"))
|
||||
elif cint(self.is_group):
|
||||
elif self.is_group:
|
||||
if self.account_type and not self.flags.exclude_account_type_check:
|
||||
throw(_("Cannot covert to Group because Account Type is selected."))
|
||||
self.validate_default_accounts_in_company()
|
||||
elif self.check_if_child_exists():
|
||||
throw(_("Account with child nodes cannot be set as ledger"))
|
||||
|
||||
def validate_default_accounts_in_company(self):
|
||||
default_account_fields = get_company_default_account_fields()
|
||||
|
||||
company_default_accounts = frappe.db.get_value(
|
||||
"Company", self.company, list(default_account_fields.keys()), as_dict=1
|
||||
)
|
||||
|
||||
msg = _("Account {0} cannot be disabled as it is already set as {1} for {2}.")
|
||||
|
||||
if not self.disabled:
|
||||
msg = _("Account {0} cannot be converted to Group as it is already set as {1} for {2}.")
|
||||
|
||||
for d in default_account_fields:
|
||||
if company_default_accounts.get(d) == self.name:
|
||||
throw(
|
||||
msg.format(
|
||||
frappe.bold(self.name),
|
||||
frappe.bold(default_account_fields.get(d)),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_frozen_accounts_modifier(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||
return
|
||||
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
||||
throw(_("You are not authorized to set Frozen value"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
@@ -337,9 +302,7 @@ class Account(NestedSet):
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value(
|
||||
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
|
||||
)
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
@@ -516,7 +479,6 @@ def get_account_autoname(account_number, account_name, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
if not account:
|
||||
return
|
||||
@@ -537,7 +499,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name and not from_descendant:
|
||||
if old_name:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -578,7 +540,6 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -632,55 +593,3 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.in_test:
|
||||
return
|
||||
|
||||
last_gl_update = None
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if not last_gl_update:
|
||||
return
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
|
||||
def get_company_default_account_fields():
|
||||
return {
|
||||
"default_bank_account": "Default Bank Account",
|
||||
"default_cash_account": "Default Cash Account",
|
||||
"default_receivable_account": "Default Receivable Account",
|
||||
"default_payable_account": "Default Payable Account",
|
||||
"default_expense_account": "Default Expense Account",
|
||||
"default_income_account": "Default Income Account",
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
|
||||
"stock_adjustment_account": "Stock Adjustment Account",
|
||||
"write_off_account": "Write Off Account",
|
||||
"default_discount_account": "Default Payment Discount Account",
|
||||
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
|
||||
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
|
||||
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
|
||||
"round_off_account": "Round Off Account",
|
||||
"default_deferred_revenue_account": "Default Deferred Revenue Account",
|
||||
"default_deferred_expense_account": "Default Deferred Expense Account",
|
||||
"accumulated_depreciation_account": "Accumulated Depreciation Account",
|
||||
"depreciation_expense_account": "Depreciation Expense Account",
|
||||
"disposal_account": "Gain/Loss Account on Asset Disposal",
|
||||
}
|
||||
|
||||
@@ -139,11 +139,6 @@ frappe.treeview_settings["Account"] = {
|
||||
description: __(
|
||||
"Further accounts can be made under Groups, but entries can be made against non-Groups"
|
||||
),
|
||||
onchange: function () {
|
||||
if (!this.value) {
|
||||
this.layout.set_value("root_type", "");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
@@ -160,14 +155,6 @@ frappe.treeview_settings["Account"] = {
|
||||
.options,
|
||||
description: __("Optional. This setting will be used to filter in various transactions."),
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "account_category",
|
||||
label: __("Account Category"),
|
||||
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
|
||||
.options,
|
||||
description: __("Optional. Used with Financial Report Template"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "tax_rate",
|
||||
@@ -278,14 +265,12 @@ frappe.treeview_settings["Account"] = {
|
||||
label: __("View Ledger"),
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
if (node.parent_label) {
|
||||
frappe.route_options["account"] = node.label;
|
||||
}
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
|
||||
@@ -18,12 +18,19 @@ def create_charts(
|
||||
accounts = []
|
||||
|
||||
def _import_accounts(children, parent, root_type, root_account=False):
|
||||
nonlocal custom_chart
|
||||
for account_name, child in children.items():
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
if account_name not in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
@@ -47,10 +54,8 @@ def create_charts(
|
||||
"report_type": report_type,
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_category": child.get("account_category"),
|
||||
"account_currency": child.get("account_currency")
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
or frappe.get_cached_value("Company", company, "default_currency"),
|
||||
"tax_rate": child.get("tax_rate"),
|
||||
}
|
||||
)
|
||||
@@ -90,7 +95,20 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(set(child.keys()) - set(get_chart_metadata_fields())):
|
||||
elif len(
|
||||
set(child.keys())
|
||||
- set(
|
||||
[
|
||||
"account_name",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
]
|
||||
)
|
||||
):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
@@ -98,7 +116,6 @@ def identify_is_group(child):
|
||||
return is_group
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_chart(chart_template, existing_company=None):
|
||||
chart = {}
|
||||
if existing_company:
|
||||
@@ -233,7 +250,13 @@ def validate_bank_account(coa, bank_account):
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
if account_name not in [
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
@@ -258,7 +281,15 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"""recursively called to form a parent-child based list of dict from chart template"""
|
||||
for account_name, child in children.items():
|
||||
account = {}
|
||||
if account_name in get_chart_metadata_fields():
|
||||
if account_name in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
continue
|
||||
|
||||
if from_coa_importer:
|
||||
@@ -276,16 +307,3 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
|
||||
_import_accounts(chart, None)
|
||||
return accounts
|
||||
|
||||
|
||||
def get_chart_metadata_fields():
|
||||
return [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"account_category",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]
|
||||
|
||||
@@ -1,817 +0,0 @@
|
||||
{
|
||||
"country_code": "au",
|
||||
"name": "Australia - Chart of Accounts with Account Numbers",
|
||||
"tree": {
|
||||
"Assets": {
|
||||
"Current Assets": {
|
||||
"Cash On Hand": {
|
||||
"Cash On Hand": {
|
||||
"account_number": "11010",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"account_number": "110",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash at Bank": {
|
||||
"Every Day Bank Account": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11520"
|
||||
},
|
||||
"Business Term Deposit": {
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
"account_number": "12010",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "12020"
|
||||
},
|
||||
"Sundry Debtors": {
|
||||
"account_number": "12030"
|
||||
},
|
||||
"Debtor Refund": {
|
||||
"account_number": "12040"
|
||||
},
|
||||
"account_number": "120",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock On Hand": {
|
||||
"account_number": "13010",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"WIP - Work In Progress - Manufacturing": {
|
||||
"account_number": "13020"
|
||||
},
|
||||
"account_number": "130",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "14010"
|
||||
},
|
||||
"Provisional Tax Paid": {
|
||||
"account_number": "14020"
|
||||
},
|
||||
"account_number": "140",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "11",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16010",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Plant & Equipment": {
|
||||
"account_number": "16020",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "160",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicle": {
|
||||
"Motor Vehicle": {
|
||||
"account_number": "16110",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Motor Vehicle": {
|
||||
"account_number": "16120",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "161",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Furniture & Equipment": {
|
||||
"account_number": "16210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Office Furniture & Equipment": {
|
||||
"account_number": "16220",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "162",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16310",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Computer Equipment": {
|
||||
"account_number": "16320",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "163",
|
||||
"is_group": 1
|
||||
},
|
||||
"Building": {
|
||||
"Buildings": {
|
||||
"account_number": "16410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Buildings": {
|
||||
"account_number": "16420",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"CWIP - Construction Work In Progress": {
|
||||
"account_number": "16430",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"Accumulated Depreciation - Others": {
|
||||
"account_number": "16440",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "164",
|
||||
"is_group": 1
|
||||
},
|
||||
"Related Party": {
|
||||
"Loan to Party 1": {
|
||||
"account_number": "17010"
|
||||
},
|
||||
"account_number": "170",
|
||||
"is_group": 1
|
||||
},
|
||||
"Investments & Unlisted Entities": {
|
||||
"Investment - Entity 1": {
|
||||
"account_number": "17510"
|
||||
},
|
||||
"account_number": "175",
|
||||
"is_group": 1
|
||||
},
|
||||
"Intagible Assets": {
|
||||
"Goodwill": {
|
||||
"account_number": "18010"
|
||||
},
|
||||
"Opening Balance Temporary ": {
|
||||
"account_number": "18090",
|
||||
"account_type": "Temporary"
|
||||
},
|
||||
"account_number": "180",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "16",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "1",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Liabilities": {
|
||||
"Current Liabilities": {
|
||||
"Trade Payables - Current": {
|
||||
"Trade Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21050",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21060"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21070",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"account_number": "210",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Payables - Current": {
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21510"
|
||||
},
|
||||
"Payroll - Wages Clearing": {
|
||||
"account_number": "21550"
|
||||
},
|
||||
"Payroll - Superannuation Deductions": {
|
||||
"account_number": "21555"
|
||||
},
|
||||
"Payroll - Misc Deductions": {
|
||||
"account_number": "21560"
|
||||
},
|
||||
"Payroll - Withholding Tax Payable": {
|
||||
"account_number": "21565"
|
||||
},
|
||||
"account_number": "215",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST": {
|
||||
"GST Payments to ATO": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Provision for PAYG Tax": {
|
||||
"account_number": "22040"
|
||||
},
|
||||
"account_number": "220",
|
||||
"account_type": "Tax",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Current": {
|
||||
"Credit Card - VISA": {
|
||||
"account_number": "22510"
|
||||
},
|
||||
"account_number": "225",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Overdraft": {
|
||||
"Bank Overdraft Cash at Bank": {
|
||||
"account_number": "23010"
|
||||
},
|
||||
"account_number": "230",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Finance": {
|
||||
"Trade Finance": {
|
||||
"account_number": "23510"
|
||||
},
|
||||
"account_number": "235",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Finance Lease - Current": {
|
||||
"account_number": "24010"
|
||||
},
|
||||
"account_number": "240",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "24510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "24520"
|
||||
},
|
||||
"account_number": "245",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Liabilities": {
|
||||
"Trade & Other Payables - Non Current": {
|
||||
"Loan Account - Party 1": {
|
||||
"account_number": "25010"
|
||||
},
|
||||
"account_number": "250",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Non Current": {
|
||||
"Non Current Liability - Director Loan": {
|
||||
"account_number": "25510"
|
||||
},
|
||||
"account_number": "255",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Loans - Non Current": {
|
||||
"Bank Loan 1 - Non Current": {
|
||||
"account_number": "26010"
|
||||
},
|
||||
"account_number": "260",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities - Non Current": {
|
||||
"Finance Lease - Non Current": {
|
||||
"account_number": "27010"
|
||||
},
|
||||
"account_number": "270",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions - Non Current": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "27510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "27520"
|
||||
},
|
||||
"account_number": "275",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "2",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Equity": {
|
||||
"Owner's/Shareholder's Equity": {
|
||||
"Owner's/Shareholders Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Owner's/Shareholders Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "310",
|
||||
"is_group": 1
|
||||
},
|
||||
"Earnings": {
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "350",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "31",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "3",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Revenue": {
|
||||
"Revenue": {
|
||||
"Sales Revenue": {
|
||||
"Sales Income": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Freight Income": {
|
||||
"account_number": "41020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"account_number": "41030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Service Income": {
|
||||
"account_number": "41040",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "410",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Revenue": {
|
||||
"Commission Received": {
|
||||
"account_number": "42010"
|
||||
},
|
||||
"Discounts Received": {
|
||||
"account_number": "42020"
|
||||
},
|
||||
"Interest received": {
|
||||
"account_number": "42030"
|
||||
},
|
||||
"Profit/Loss on Sales of Assets": {
|
||||
"account_number": "42040"
|
||||
},
|
||||
"Rent Received": {
|
||||
"account_number": "42050"
|
||||
},
|
||||
"Sundry Income": {
|
||||
"account_number": "42060"
|
||||
},
|
||||
"account_number": "420",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "41",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "4",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods Sold": {
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Expenses (sales related)": {
|
||||
"account_number": "51020"
|
||||
},
|
||||
"Discounts Given": {
|
||||
"account_number": "51030"
|
||||
},
|
||||
"Subcontracting Charges": {
|
||||
"account_number": "51040"
|
||||
},
|
||||
"account_number": "510",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other COGS": {
|
||||
"Purchases - Miscellaneous": {
|
||||
"account_number": "52010"
|
||||
},
|
||||
"Duty & Customs Fees": {
|
||||
"account_number": "52020",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "52030",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "52040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Wirte Off": {
|
||||
"account_number": "52050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Valuation Expenses": {
|
||||
"account_number": "52060",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Asset Valuation Expenses": {
|
||||
"account_number": "52070",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "520",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "51",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "5",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Expenses": {
|
||||
"Fixed Expenses": {
|
||||
"Payroll & Related Expenses": {
|
||||
"Salaries & Wages": {
|
||||
"account_number": "61010"
|
||||
},
|
||||
"Superannuation": {
|
||||
"account_number": "61015"
|
||||
},
|
||||
"Staff Amenities - GST Paid": {
|
||||
"account_number": "61020"
|
||||
},
|
||||
"Staff Amenities - GST Free": {
|
||||
"account_number": "61025"
|
||||
},
|
||||
"Staff Recruitment": {
|
||||
"account_number": "61030"
|
||||
},
|
||||
"Staff Training": {
|
||||
"account_number": "61035"
|
||||
},
|
||||
"Fringe Benefits Tax": {
|
||||
"account_number": "61040"
|
||||
},
|
||||
"Payroll Tax": {
|
||||
"account_number": "61045"
|
||||
},
|
||||
"Workers Compensation": {
|
||||
"account_number": "61050"
|
||||
},
|
||||
"Long Service Leave": {
|
||||
"account_number": "61060"
|
||||
},
|
||||
"Mileage Reimbursement": {
|
||||
"account_number": "61070"
|
||||
},
|
||||
"Overtime": {
|
||||
"account_number": "61080"
|
||||
},
|
||||
"Worksafe Insurance": {
|
||||
"account_number": "61090"
|
||||
},
|
||||
"account_number": "610",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation Expenses": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicle": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Building": {
|
||||
"account_number": "62050",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Others": {
|
||||
"account_number": "62510",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "620",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "61",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses - Salaries & Wages": {
|
||||
"account_number": "63010"
|
||||
},
|
||||
"Accrued Expenses - Interest": {
|
||||
"account_number": "63020"
|
||||
},
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"General and Administrative Expenses": {
|
||||
"Low Value Assets less than $300": {
|
||||
"account_number": "64010"
|
||||
},
|
||||
"Office Supplies": {
|
||||
"account_number": "64020"
|
||||
},
|
||||
"Postage & Courier": {
|
||||
"account_number": "64025"
|
||||
},
|
||||
"Printing & Stationery": {
|
||||
"account_number": "64030"
|
||||
},
|
||||
"Registration Fees / Filing Fees": {
|
||||
"account_number": "64040"
|
||||
},
|
||||
"Travel & Accommodation - Local": {
|
||||
"account_number": "64050"
|
||||
},
|
||||
"Travel & Accommodation - Overseas": {
|
||||
"account_number": "64060"
|
||||
},
|
||||
"Relocation Costs": {
|
||||
"account_number": "64070"
|
||||
},
|
||||
"Hire Charges": {
|
||||
"account_number": "64080"
|
||||
},
|
||||
"Repairs & Maintenance": {
|
||||
"account_number": "64210"
|
||||
},
|
||||
"Cleaning Expenses": {
|
||||
"account_number": "64215"
|
||||
},
|
||||
"Uniforms": {
|
||||
"account_number": "64220"
|
||||
},
|
||||
"Security": {
|
||||
"account_number": "64225"
|
||||
},
|
||||
"Subscriptions & Licences": {
|
||||
"account_number": "64510"
|
||||
},
|
||||
"Software Expenses": {
|
||||
"account_number": "64515"
|
||||
},
|
||||
"Marketing Expenses": {
|
||||
"account_number": "64520"
|
||||
},
|
||||
"Advertising Expenses": {
|
||||
"account_number": "64525"
|
||||
},
|
||||
"Website Hosting & Domain Expenses": {
|
||||
"account_number": "64530"
|
||||
},
|
||||
"Computer Repairs / Supplies": {
|
||||
"account_number": "64540"
|
||||
},
|
||||
"Conferences": {
|
||||
"account_number": "64550"
|
||||
},
|
||||
"Consultancy /Contract Services": {
|
||||
"account_number": "64560"
|
||||
},
|
||||
"Training Services": {
|
||||
"account_number": "64570"
|
||||
},
|
||||
"Workshop Supplies": {
|
||||
"account_number": "64580"
|
||||
},
|
||||
"Consumables": {
|
||||
"account_number": "64585"
|
||||
},
|
||||
"Entertainment Expenses - Deductible": {
|
||||
"account_number": "64810"
|
||||
},
|
||||
"Entertainment Expenses - Non Deductible": {
|
||||
"account_number": "64820"
|
||||
},
|
||||
"Amortisation Of Goodwill": {
|
||||
"account_number": "64910"
|
||||
},
|
||||
"General / Miscellaneous Expenses": {
|
||||
"account_number": "64915",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Donations": {
|
||||
"account_number": "64920"
|
||||
},
|
||||
"Client Gifts": {
|
||||
"account_number": "64930"
|
||||
},
|
||||
"Employee Gifts": {
|
||||
"account_number": "64935"
|
||||
},
|
||||
"account_number": "640",
|
||||
"is_group": 1
|
||||
},
|
||||
"Occupancy Expenses": {
|
||||
"Rental Expenses": {
|
||||
"account_number": "65010"
|
||||
},
|
||||
"Property Insurance": {
|
||||
"account_number": "65020"
|
||||
},
|
||||
"Electricity Expenses": {
|
||||
"account_number": "65030"
|
||||
},
|
||||
"Water Rates": {
|
||||
"account_number": "65040"
|
||||
},
|
||||
"Gas Expenses": {
|
||||
"account_number": "65050"
|
||||
},
|
||||
"Property Taxes": {
|
||||
"account_number": "65060"
|
||||
},
|
||||
"Rates": {
|
||||
"account_number": "65070"
|
||||
},
|
||||
"account_number": "650",
|
||||
"is_group": 1
|
||||
},
|
||||
"Communication & Vehicle Expenses": {
|
||||
"Internet Expenses": {
|
||||
"account_number": "66010"
|
||||
},
|
||||
"Mobile Telephone": {
|
||||
"account_number": "66020"
|
||||
},
|
||||
"Telephone Expenses": {
|
||||
"account_number": "66030"
|
||||
},
|
||||
"Motor Vehicle - Fuel Expenses": {
|
||||
"account_number": "66040"
|
||||
},
|
||||
"Motor Vehicle - Parking & Tolls": {
|
||||
"account_number": "66050"
|
||||
},
|
||||
"Motor Vehicle - Registration & Insurance": {
|
||||
"account_number": "66060"
|
||||
},
|
||||
"Motor Vehicle - Service & Repairs": {
|
||||
"account_number": "66070"
|
||||
},
|
||||
"Taxi": {
|
||||
"account_number": "66080"
|
||||
},
|
||||
"account_number": "660",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "64",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Operating Expenses": {
|
||||
"Finance Costs": {
|
||||
"Interest - Bank Loans": {
|
||||
"account_number": "67010"
|
||||
},
|
||||
"Interest - Finance Leases": {
|
||||
"account_number": "67020"
|
||||
},
|
||||
"Interest - Other Loans": {
|
||||
"account_number": "67025"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "67030"
|
||||
},
|
||||
"Bank Charges": {
|
||||
"account_number": "67050"
|
||||
},
|
||||
"Rounding off": {
|
||||
"account_number": "67055",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Audit Fees": {
|
||||
"account_number": "67060"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "67070"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "67080"
|
||||
},
|
||||
"Management Fees": {
|
||||
"account_number": "67090"
|
||||
},
|
||||
"account_number": "670",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Costs": {
|
||||
"Doubtful Debts": {
|
||||
"account_number": "67510"
|
||||
},
|
||||
"Fines": {
|
||||
"account_number": "67520"
|
||||
},
|
||||
"Debt Collection": {
|
||||
"account_number": "67530"
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "67540"
|
||||
},
|
||||
"account_number": "675",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "67",
|
||||
"is_group": 1
|
||||
},
|
||||
"Variable Expenses": {
|
||||
"Variable Expenses": {
|
||||
"Bonus & Commissions Paid": {
|
||||
"account_number": "68010"
|
||||
},
|
||||
"Bonus & Commissions To be Paid": {
|
||||
"account_number": "68020"
|
||||
},
|
||||
"Warranty Claims": {
|
||||
"account_number": "68030"
|
||||
},
|
||||
"account_number": "680",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "68",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "6",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Other Income": {
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "71010"
|
||||
},
|
||||
"account_number": "710",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Income": {
|
||||
"Gain on Asset Disposal": {
|
||||
"account_number": "73010"
|
||||
},
|
||||
"account_number": "730",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "71",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "7",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Other Expenses": {
|
||||
"Other Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"account_number": "81010"
|
||||
},
|
||||
"account_number": "810",
|
||||
"is_group": 1
|
||||
},
|
||||
"Foreign Exchange Gain/Loss": {
|
||||
"Exchange Loss/Gain - Realized": {
|
||||
"account_number": "82010"
|
||||
},
|
||||
"account_number": "820",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Expenses": {
|
||||
"Loss on Asset Disposal": {
|
||||
"account_number": "83010"
|
||||
},
|
||||
"account_number": "830",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "81",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "8",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,20 +96,8 @@
|
||||
"account_number": "1132.000"
|
||||
},
|
||||
"account_number": "1130.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1151.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
},
|
||||
"account_number": "1100.000"
|
||||
|
||||
},
|
||||
"Aktiva Tetap": {
|
||||
"Aktiva": {
|
||||
@@ -569,10 +557,6 @@
|
||||
"Hutang Pajak": {
|
||||
"account_number": "2141.000",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"PPN Keluaran": {
|
||||
"account_number": "2142.000",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "2140.000"
|
||||
},
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationery": {},
|
||||
"Print and Stationary": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -9,192 +9,103 @@ def get():
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"}
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {"account_type": "Cash", "account_category": "Cash and Cash Equivalents"},
|
||||
"account_type": "Cash",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
_("Employee Advances"): {},
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {"account_category": "Other Current Assets"}
|
||||
},
|
||||
_("Prepaid Expenses"): {"account_category": "Other Current Assets"},
|
||||
_("Short-term Investments"): {"account_category": "Short-term Investments"},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
|
||||
_("Stock In Hand"): {"account_type": "Stock"},
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_category": "Other Current Assets"},
|
||||
_("Tax Assets"): {"is_group": 1},
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_category": "Intangible Assets"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset"},
|
||||
_("Software"): {"account_type": "Fixed Asset"},
|
||||
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_category": "Long-term Investments"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_category": "Other Non-current Assets",
|
||||
}
|
||||
},
|
||||
_("Investments"): {"is_group": 1},
|
||||
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
|
||||
"root_type": "Asset",
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_category": "Other Direct Costs",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
|
||||
},
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Commission on Sales"): {"account_category": "Operating Expenses"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"},
|
||||
_("Entertainment Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Office Rent"): {"account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {"account_category": "Operating Expenses"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"},
|
||||
_("Salary"): {"account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
|
||||
_("Impairment"): {"account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_category": "Tax Expense"},
|
||||
_("Administrative Expenses"): {},
|
||||
_("Commission on Sales"): {},
|
||||
_("Depreciation"): {"account_type": "Depreciation"},
|
||||
_("Entertainment Expenses"): {},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
|
||||
_("Legal Expenses"): {},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable"},
|
||||
_("Office Maintenance Expenses"): {},
|
||||
_("Office Rent"): {},
|
||||
_("Postal Expenses"): {},
|
||||
_("Print and Stationery"): {},
|
||||
_("Round Off"): {"account_type": "Round Off"},
|
||||
_("Salary"): {},
|
||||
_("Sales Expenses"): {},
|
||||
_("Telephone Expenses"): {},
|
||||
_("Travel Expenses"): {},
|
||||
_("Utility Expenses"): {},
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
_("Impairment"): {},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_category": "Revenue from Operations"},
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
|
||||
"is_group": 1,
|
||||
},
|
||||
_("Direct Income"): {_("Sales"): {}, _("Service"): {}},
|
||||
_("Indirect Income"): {"is_group": 1},
|
||||
"root_type": "Income",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"},
|
||||
_("Payroll Payable"): {"account_category": "Other Payables"},
|
||||
_("Creditors"): {"account_type": "Payable"},
|
||||
_("Payroll Payable"): {},
|
||||
},
|
||||
_("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
|
||||
_("Customer Advances"): {"account_category": "Other Current Liabilities"},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
|
||||
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {"account_category": "Short-term Provisions"},
|
||||
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Unsecured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"},
|
||||
_("Secured Loans"): {},
|
||||
_("Unsecured Loans"): {},
|
||||
_("Bank Overdraft Account"): {},
|
||||
},
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {"account_category": "Long-term Provisions"},
|
||||
_("Employee Benefits Obligation"): {"account_category": "Other Non-current Liabilities"},
|
||||
"is_group": 1,
|
||||
},
|
||||
"root_type": "Liability",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Capital Stock"): {"account_type": "Equity"},
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,128 +10,49 @@ def get():
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {
|
||||
"account_type": "Receivable",
|
||||
"account_number": "1310",
|
||||
"account_category": "Trade Receivables",
|
||||
},
|
||||
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
|
||||
"account_number": "1300",
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_number": "1200",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {
|
||||
"account_type": "Cash",
|
||||
"account_number": "1110",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash"): {"account_type": "Cash", "account_number": "1110"},
|
||||
"account_type": "Cash",
|
||||
"account_number": "1100",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
_("Employee Advances"): {"account_number": "1610"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {
|
||||
"account_number": "1651",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Earnest Money"): {"account_number": "1651"},
|
||||
"account_number": "1650",
|
||||
},
|
||||
_("Prepaid Expenses"): {
|
||||
"account_number": "1660",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Short-term Investments"): {
|
||||
"account_number": "1670",
|
||||
"account_category": "Short-term Investments",
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {
|
||||
"account_type": "Stock",
|
||||
"account_number": "1410",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1500",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_number": "1500"},
|
||||
"account_number": "1100-1600",
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1710",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1720",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1730",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1740",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1750",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1760",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Software"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1770",
|
||||
"account_category": "Intangible Assets",
|
||||
},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipment"): {"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"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_number": "1790",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
|
||||
"account_number": "1700",
|
||||
},
|
||||
_("Investments"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1800",
|
||||
"account_category": "Long-term Investments",
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_number": "1800"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_number": "1910",
|
||||
"account_category": "Other Non-current Assets",
|
||||
},
|
||||
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
|
||||
"account_number": "1900",
|
||||
},
|
||||
"root_type": "Asset",
|
||||
@@ -140,94 +61,42 @@ def get():
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_number": "5111",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_number": "5112",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_number": "5118",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_number": "5119",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
|
||||
"account_number": "5110",
|
||||
},
|
||||
"account_number": "5100",
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {
|
||||
"account_number": "5201",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Commission on Sales"): {
|
||||
"account_number": "5202",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Depreciation"): {
|
||||
"account_type": "Depreciation",
|
||||
"account_number": "5203",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Entertainment Expenses"): {
|
||||
"account_number": "5204",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5205",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_number": "5206", "account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5207",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {
|
||||
"account_number": "5208",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Rent"): {"account_number": "5209", "account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_number": "5210", "account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {
|
||||
"account_number": "5211",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Round Off"): {
|
||||
"account_type": "Round Off",
|
||||
"account_number": "5212",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Salary"): {"account_number": "5213", "account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_number": "5214", "account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_number": "5215", "account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_number": "5216", "account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_number": "5217", "account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_number": "5218", "account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219", "account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_number": "5220", "account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_number": "5221", "account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {
|
||||
"account_number": "5222",
|
||||
"account_category": "Other Operating Income",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5223",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
|
||||
_("Administrative Expenses"): {"account_number": "5201"},
|
||||
_("Commission on Sales"): {"account_number": "5202"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
|
||||
_("Entertainment Expenses"): {"account_number": "5204"},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
|
||||
_("Legal Expenses"): {"account_number": "5206"},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
|
||||
_("Office Maintenance Expenses"): {"account_number": "5208"},
|
||||
_("Office Rent"): {"account_number": "5209"},
|
||||
_("Postal Expenses"): {"account_number": "5210"},
|
||||
_("Print and Stationery"): {"account_number": "5211"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
|
||||
_("Salary"): {"account_number": "5213"},
|
||||
_("Sales Expenses"): {"account_number": "5214"},
|
||||
_("Telephone Expenses"): {"account_number": "5215"},
|
||||
_("Travel Expenses"): {"account_number": "5216"},
|
||||
_("Utility Expenses"): {"account_number": "5217"},
|
||||
_("Write Off"): {"account_number": "5218"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
@@ -235,126 +104,54 @@ def get():
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_number": "4110", "account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_number": "4120", "account_category": "Revenue from Operations"},
|
||||
_("Sales"): {"account_number": "4110"},
|
||||
_("Service"): {"account_number": "4120"},
|
||||
"account_number": "4100",
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_number": "4210", "account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {
|
||||
"account_number": "4220",
|
||||
"account_category": "Investment Income",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "4200",
|
||||
},
|
||||
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
|
||||
"root_type": "Income",
|
||||
"account_number": "4000",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {
|
||||
"account_type": "Payable",
|
||||
"account_number": "2110",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Payroll Payable"): {"account_number": "2120", "account_category": "Other Payables"},
|
||||
_("Creditors"): {"account_type": "Payable", "account_number": "2110"},
|
||||
_("Payroll Payable"): {"account_number": "2120"},
|
||||
"account_number": "2100",
|
||||
},
|
||||
_("Accrued Expenses"): {
|
||||
"account_number": "2150",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Customer Advances"): {
|
||||
"account_number": "2160",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "2210",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_number": "2211",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
"account_number": "2200",
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
_("TDS Payable"): {
|
||||
"account_number": "2310",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("TDS Payable"): {"account_number": "2310"},
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_number": "2300",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {
|
||||
"account_number": "2350",
|
||||
"account_category": "Short-term Provisions",
|
||||
},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {
|
||||
"account_number": "2410",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Unsecured Loans"): {
|
||||
"account_number": "2420",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Bank Overdraft Account"): {
|
||||
"account_number": "2430",
|
||||
"account_category": "Short-term Borrowings",
|
||||
},
|
||||
_("Secured Loans"): {"account_number": "2410"},
|
||||
_("Unsecured Loans"): {"account_number": "2420"},
|
||||
_("Bank Overdraft Account"): {"account_number": "2430"},
|
||||
"account_number": "2400",
|
||||
},
|
||||
"account_number": "2100-2400",
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {
|
||||
"account_number": "2510",
|
||||
"account_category": "Long-term Provisions",
|
||||
},
|
||||
_("Employee Benefits Obligation"): {
|
||||
"account_number": "2520",
|
||||
"account_category": "Other Non-current Liabilities",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "2500",
|
||||
},
|
||||
"root_type": "Liability",
|
||||
"account_number": "2000",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3100",
|
||||
"account_category": "Share Capital",
|
||||
},
|
||||
_("Dividends Paid"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3200",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3300",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3400",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Revaluation Surplus"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3500",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
|
||||
"root_type": "Equity",
|
||||
"account_number": "3000",
|
||||
},
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Account",
|
||||
"name": "_Test Account 1"
|
||||
}
|
||||
]
|
||||
3
erpnext/accounts/doctype/account/test_records.toml
Normal file
3
erpnext/accounts/doctype/account/test_records.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[[Account]]
|
||||
name = "_Test Account 1"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Account Category", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:account_category_name",
|
||||
"creation": "2025-08-02 06:22:31.835063",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_category_name",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account_category_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Account Category Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-15 03:19:47.171349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_category_name, description",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document, bulk_insert
|
||||
|
||||
DOCTYPE = "Account Category"
|
||||
|
||||
|
||||
class AccountCategory(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category_name: DF.Data
|
||||
description: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FormulaFieldUpdater,
|
||||
)
|
||||
|
||||
# get all template rows with this account category being used
|
||||
row = frappe.qb.DocType("Financial Report Row")
|
||||
rows = frappe._dict(
|
||||
frappe.qb.from_(row)
|
||||
.select(row.name, row.calculation_formula)
|
||||
.where(row.calculation_formula.like(f"%{old_name}%"))
|
||||
.run()
|
||||
)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# Update formulas with new name
|
||||
updater = FormulaFieldUpdater(
|
||||
field_name="account_category",
|
||||
value_mapping={old_name: new_name},
|
||||
exclude_operators=["like", "not like"],
|
||||
)
|
||||
|
||||
updated_formulas = updater.update_in_rows(rows)
|
||||
|
||||
if updated_formulas:
|
||||
frappe.msgprint(
|
||||
_("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
|
||||
)
|
||||
|
||||
|
||||
def import_account_categories(template_path: str):
|
||||
categories_file = os.path.join(template_path, "account_categories.json")
|
||||
|
||||
if not os.path.exists(categories_file):
|
||||
return
|
||||
|
||||
with open(categories_file) as f:
|
||||
categories = json.load(f, object_hook=frappe._dict)
|
||||
|
||||
create_account_categories(categories)
|
||||
|
||||
|
||||
def create_account_categories(categories: list[dict]):
|
||||
if not categories:
|
||||
return
|
||||
|
||||
existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
|
||||
new_categories = []
|
||||
|
||||
for category_data in categories:
|
||||
category_name = category_data.get("account_category_name")
|
||||
if not category_name or category_name in existing_categories:
|
||||
continue
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
**category_data,
|
||||
"doctype": DOCTYPE,
|
||||
"name": category_name,
|
||||
}
|
||||
)
|
||||
|
||||
new_categories.append(doc)
|
||||
existing_categories.add(category_name)
|
||||
|
||||
if new_categories:
|
||||
bulk_insert(DOCTYPE, new_categories)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestAccountCategory(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for AccountCategory.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -11,9 +11,6 @@
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"reporting_currency_exchange_rate",
|
||||
"debit_in_reporting_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
@@ -127,30 +124,12 @@
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
"modified": "2024-03-27 13:05:56.710541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
@@ -179,8 +158,7 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
@@ -29,15 +26,12 @@ class AccountClosingBalance(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
is_period_closing_voucher_entry: DF.Check
|
||||
period_closing_voucher: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -61,7 +55,6 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
@@ -151,29 +144,3 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
|
||||
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||
if not reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, closing_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||
|
||||
cle.update(
|
||||
{
|
||||
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestAccountClosingBalance(UnitTestCase):
|
||||
"""
|
||||
Unit tests for AccountClosingBalance.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestAccountClosingBalance(IntegrationTestCase):
|
||||
|
||||
@@ -83,7 +83,7 @@ class AccountingDimension(Document):
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
@@ -91,7 +91,7 @@ class AccountingDimension(Document):
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
@@ -111,15 +111,17 @@ class AccountingDimension(Document):
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
count = 0
|
||||
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
|
||||
for doctype in doclist:
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
"label": doc.label,
|
||||
@@ -211,7 +213,7 @@ def delete_accounting_dimension(doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def disable_dimension(doc):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
toggle_disabling(doc=doc)
|
||||
else:
|
||||
frappe.enqueue(toggle_disabling, doc=doc)
|
||||
@@ -309,8 +311,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -58,10 +58,6 @@ class TestAccountingDimension(IntegrationTestCase):
|
||||
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_mandatory(self):
|
||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||
location.dimension_defaults[0].mandatory_for_bs = True
|
||||
location.save()
|
||||
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
si.append(
|
||||
"items",
|
||||
@@ -125,6 +121,7 @@ def create_dimension():
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Location",
|
||||
"default_dimension": "Block 1",
|
||||
"mandatory_for_bs": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounting_dimension",
|
||||
"fieldname",
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
@@ -91,17 +90,11 @@
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
"modified": "2024-03-27 13:05:57.199186",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@@ -146,9 +139,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,17 @@ class AccountingDimensionFilter(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
|
||||
ApplicableOnAccount,
|
||||
)
|
||||
|
||||
accounting_dimension: DF.Literal[None]
|
||||
accounting_dimension: DF.Literal
|
||||
accounts: DF.Table[ApplicableOnAccount]
|
||||
allow_or_restrict: DF.Literal["Allow", "Restrict"]
|
||||
apply_restriction_on_values: DF.Check
|
||||
company: DF.Link
|
||||
dimensions: DF.Table[AllowedDimension]
|
||||
disabled: DF.Check
|
||||
fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -36,10 +37,6 @@ class AccountingDimensionFilter(Document):
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.fieldname = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
|
||||
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
|
||||
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
def validate_applicable_accounts(self):
|
||||
@@ -74,7 +71,7 @@ def get_dimension_filter_map():
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, p.fieldname, a.is_mandatory
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
@@ -89,6 +86,8 @@ def get_dimension_filter_map():
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -50,13 +49,6 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -70,11 +62,10 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -114,9 +105,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
@@ -117,7 +116,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
|
||||
@@ -22,32 +22,4 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
if (frm.doc[field_name]) {
|
||||
const other_field =
|
||||
field_name === "add_taxes_from_item_tax_template"
|
||||
? "add_taxes_from_taxes_and_charges_template"
|
||||
: "add_taxes_from_item_tax_template";
|
||||
frm.set_value(other_field, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"confirm_before_resetting_posting_date",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -32,7 +31,6 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"add_taxes_from_taxes_and_charges_template",
|
||||
"book_tax_discount_loss",
|
||||
"round_row_wise_tax",
|
||||
"print_settings",
|
||||
@@ -40,15 +38,8 @@
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -67,18 +58,17 @@
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"role_to_notify_on_depreciation_failure",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
@@ -87,20 +77,28 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"use_legacy_budget_controller"
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -534,130 +532,14 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_internal_transaction_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_internal_transaction_rate",
|
||||
"fieldname": "maintain_same_rate_action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
|
||||
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||
"fieldname": "role_to_override_stop_action",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
"fieldname": "confirm_before_resetting_posting_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Confirm before resetting posting date"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_price_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Price Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System will do an implicit conversion using the pegged currency. <br>\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.",
|
||||
"documentation_url": "/app/pegged-currencies/Pegged Currencies",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Implicit Pegged Currency Conversion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-03 20:42:13.238050",
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -682,9 +564,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -22,10 +23,9 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
@@ -37,7 +37,6 @@ class AccountsSettings(Document):
|
||||
book_tax_discount_loss: DF.Check
|
||||
calculate_depr_using_total_days: DF.Check
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
@@ -47,22 +46,17 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
maintain_same_internal_transaction_rate: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
@@ -72,12 +66,9 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
use_legacy_controller_for_pcv: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_tax_settings()
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
@@ -98,6 +89,9 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -124,6 +118,10 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
@@ -137,21 +135,3 @@ class AccountsSettings(Document):
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
def validate_auto_tax_settings(self):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
refresh(frm) {
|
||||
frm.set_currency_labels(["amount"], frm.doc.currency);
|
||||
frm.set_currency_labels(["base_amount"], erpnext.get_currency(frm.doc.company));
|
||||
},
|
||||
});
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
||||
@@ -10,12 +10,9 @@
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"amount",
|
||||
"base_amount",
|
||||
"event",
|
||||
"delinked"
|
||||
"currency",
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -71,36 +68,12 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "base_amount",
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_rate",
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"precision": "9",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-13 12:45:03.014555",
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
@@ -134,8 +107,7 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.utils import get_advance_payment_doctypes, update_voucher_outstanding
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -19,32 +17,11 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
base_amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
exchange_rate: DF.Float
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_update(self):
|
||||
if (
|
||||
self.against_voucher_type in get_advance_payment_doctypes()
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["against_voucher_type", "against_voucher_no"],
|
||||
)
|
||||
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["voucher_type", "voucher_no"],
|
||||
)
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -96,13 +95,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -132,8 +132,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
"length": 30
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -209,7 +208,6 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -252,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -284,10 +282,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -71,6 +72,35 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
@@ -79,7 +109,6 @@ class BankAccount(Document):
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"company": self.company,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
@@ -88,6 +117,15 @@ class BankAccount(Document):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
|
||||
@@ -8,4 +8,38 @@ from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankAccount(IntegrationTestCase):
|
||||
pass
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@@ -89,64 +89,46 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
invalid_document = []
|
||||
invalid_cheque_date = []
|
||||
entries_to_update = []
|
||||
|
||||
def validate_entry(d):
|
||||
is_valid = True
|
||||
if not d.payment_document:
|
||||
invalid_document.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
invalid_cheque_date.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
|
||||
clearance_date_updated = False
|
||||
for d in self.get("payment_entries"):
|
||||
if validate_entry(d) and (d.clearance_date or self.include_reconciled_entries):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
if d.clearance_date or self.include_reconciled_entries:
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
entries_to_update.append(d)
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
|
||||
if invalid_document or invalid_cheque_date:
|
||||
msg = _("<p>Please correct the following row(s):</p><ul>")
|
||||
if invalid_document:
|
||||
msg += _("<li>Payment document required for row(s): {0}</li>").format(
|
||||
", ".join(invalid_document)
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
if invalid_cheque_date:
|
||||
msg += _("<li>Clearance date must be after cheque date for row(s): {0}</li>").format(
|
||||
", ".join(invalid_cheque_date)
|
||||
)
|
||||
clearance_date_updated = True
|
||||
|
||||
msg += "</ul>"
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
if clearance_date_updated:
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
else:
|
||||
msgprint(_("Clearance Date not mentioned"))
|
||||
return
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
|
||||
def get_payment_entries_for_bank_clearance(
|
||||
@@ -155,10 +137,8 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -180,28 +160,31 @@ def get_payment_entries_for_bank_clearance(
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
posting_date ASC, name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
"bank_account": bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -7,9 +7,6 @@ from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
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.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -146,7 +143,7 @@ def make_payment_entry():
|
||||
|
||||
supplier = create_supplier(supplier_name="_Test Supplier")
|
||||
pi = make_purchase_invoice(
|
||||
supplier=supplier.name,
|
||||
supplier=supplier,
|
||||
supplier_warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
uom="Nos",
|
||||
@@ -175,13 +172,11 @@ def make_pos_sales_invoice():
|
||||
|
||||
customer = make_customer(customer="_Test Customer")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
|
||||
|
||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si.set("payments", [])
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
||||
)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("bank", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -215,10 +214,9 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified": "2024-03-27 13:06:37.731207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
@@ -252,10 +250,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "customer",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "customer"
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -373,43 +372,15 @@ def auto_reconcile_vouchers(
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
for bank_transaction_batch in create_batch(bank_transactions, 1000):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transaction_batch,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
bank_transactions,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
|
||||
|
||||
def start_auto_reconcile(
|
||||
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
["payment_entry", "journal_entry"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -443,6 +414,7 @@ def start_auto_reconcile(
|
||||
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
return reconciled, partially_reconciled
|
||||
|
||||
|
||||
def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||
@@ -519,23 +491,16 @@ def subtract_allocations(gl_account, vouchers):
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
if amount := get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
voucher["paid_amount"] -= amount
|
||||
|
||||
copied.append(voucher)
|
||||
return copied
|
||||
|
||||
|
||||
def get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||
if not (voucher_details := voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name")))):
|
||||
return
|
||||
|
||||
if not (row := voucher_details.get(gl_account)):
|
||||
return
|
||||
|
||||
return row.get("total")
|
||||
|
||||
|
||||
def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
@@ -666,7 +631,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -805,20 +770,26 @@ def get_je_matching_query(
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
ref_condition = je.cheque_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
subquery = (
|
||||
query = (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
Sum(getattr(jea, amount_field)).as_("paid_amount"),
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
getattr(jea, amount_field).as_("paid_amount"),
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("reference_date"),
|
||||
je.pay_to_recd_from.as_("party"),
|
||||
@@ -830,38 +801,22 @@ def get_je_matching_query(
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(filter_by_date)
|
||||
.groupby(je.name)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
subquery = subquery.where(je.cheque_no == transaction.reference_number)
|
||||
|
||||
ref_rank = frappe.qb.terms.Case().when(subquery.reference_no == transaction.reference_number, 1).else_(0)
|
||||
amount_equality = subquery.paid_amount == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(subquery)
|
||||
.select(
|
||||
"*",
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
)
|
||||
.where(amount_equality if exact_match else subquery.paid_amount > 0.0)
|
||||
)
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -874,11 +829,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -892,9 +847,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestBankReconciliationTool(AccountsTestMixin, IntegrationTestCase):
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "HDFC _current_",
|
||||
"bank": bank.name,
|
||||
"bank": bank,
|
||||
"is_company_account": True,
|
||||
"account": self.bank, # account from Chart of Accounts
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
frm.get_field("import_file").df.options = {
|
||||
restrictions: {
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx", ".TXT", ".txt"],
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -81,7 +81,6 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
refresh(frm) {
|
||||
frm.page.hide_icon_group();
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.trigger("update_indicators");
|
||||
frm.trigger("import_file");
|
||||
frm.trigger("show_import_log");
|
||||
@@ -193,24 +192,6 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
});
|
||||
},
|
||||
|
||||
import_mt940_fromat(frm) {
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.save();
|
||||
},
|
||||
|
||||
toggle_mt940_note(frm) {
|
||||
if (!frm.doc.import_mt940_fromat) {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 0);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 0);
|
||||
frm.set_df_property("html_5", "hidden", 0);
|
||||
} else {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 1);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 1);
|
||||
frm.set_df_property("html_5", "hidden", 1);
|
||||
}
|
||||
frm.set_value("import_mt940_fromat", frm.doc.import_mt940_fromat);
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
if (frm.doc.status === "Error") {
|
||||
frappe.db
|
||||
@@ -252,7 +233,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
open_url_post(method, {
|
||||
doctype: "Bank Transaction",
|
||||
export_records: "blank_template",
|
||||
export_records: "5_records",
|
||||
export_fields: {
|
||||
"Bank Transaction": [
|
||||
"date",
|
||||
@@ -309,45 +290,23 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
.html(__("Loading import file..."))
|
||||
.appendTo(frm.get_field("import_preview").$wrapper);
|
||||
|
||||
frappe.run_serially([
|
||||
// Convert MT940 to CSV if .txt file
|
||||
() => {
|
||||
if (frm.doc.import_file && frm.doc.import_file.toLowerCase().endsWith(".txt")) {
|
||||
return frm
|
||||
.call({
|
||||
method: "convert_mt940_to_csv",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
mt940_file_path: frm.doc.import_file,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const file_url = r.message;
|
||||
frm.set_value("import_file", file_url);
|
||||
frm.save();
|
||||
});
|
||||
}
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
},
|
||||
() => {
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
},
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
},
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
},
|
||||
]);
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
},
|
||||
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"bank_account",
|
||||
"bank",
|
||||
"column_break_4",
|
||||
"import_mt940_fromat",
|
||||
"custom_delimiters",
|
||||
"delimiter_options",
|
||||
"google_sheets_url",
|
||||
@@ -21,7 +20,6 @@
|
||||
"download_template",
|
||||
"status",
|
||||
"template_options",
|
||||
"use_csv_sniffer",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
@@ -209,28 +207,14 @@
|
||||
"fieldname": "delimiter_options",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delimiter options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_csv_sniffer",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Use CSV Sniffer"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "import_mt940_fromat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Import MT940 Fromat"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-11 02:23:22.159961",
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -246,9 +230,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,15 @@
|
||||
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
|
||||
import frappe
|
||||
import mt940
|
||||
import openpyxl
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.data_import import DataImport
|
||||
from frappe.core.doctype.data_import.importer import Importer, ImportFile
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.file_manager import get_file, save_file
|
||||
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
@@ -40,7 +35,6 @@ class BankStatementImport(DataImport):
|
||||
delimiter_options: DF.Data | None
|
||||
google_sheets_url: DF.Data | None
|
||||
import_file: DF.Attach | None
|
||||
import_mt940_fromat: DF.Check
|
||||
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
|
||||
mute_emails: DF.Check
|
||||
reference_doctype: DF.Link
|
||||
@@ -49,7 +43,6 @@ class BankStatementImport(DataImport):
|
||||
submit_after_import: DF.Check
|
||||
template_options: DF.Code | None
|
||||
template_warnings: DF.Code | None
|
||||
use_csv_sniffer: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -72,9 +65,8 @@ class BankStatementImport(DataImport):
|
||||
|
||||
self.template_warnings = ""
|
||||
|
||||
if self.import_file and not self.import_file.lower().endswith(".txt"):
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
@@ -87,7 +79,7 @@ class BankStatementImport(DataImport):
|
||||
from frappe.utils.background_jobs import is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
run_now = frappe.in_test or frappe.conf.developer_mode
|
||||
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||
if is_scheduler_inactive() and not run_now:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
@@ -112,102 +104,6 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""Preprocess MT940 content to fix statement number format issues.
|
||||
|
||||
The MT940 standard expects statement numbers to be maximum 5 digits,
|
||||
but some banks provide longer statement numbers that cause parsing errors.
|
||||
This function truncates statement numbers longer than 5 digits to the last 5 digits.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
|
||||
|
||||
def replace_statement_number(match):
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or "" # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or "" # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
if not transactions:
|
||||
frappe.throw(_("Parsed file is not in valid MT940 format or contains no transactions."))
|
||||
|
||||
# Use in-memory file buffer instead of writing to temp file
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer)
|
||||
|
||||
headers = ["Date", "Deposit", "Withdrawal", "Description", "Reference Number", "Bank Account", "Currency"]
|
||||
writer.writerow(headers)
|
||||
|
||||
for txn in transactions:
|
||||
txn_date = getattr(txn, "date", None)
|
||||
raw_date = txn.data.get("date", "")
|
||||
|
||||
if txn_date:
|
||||
date_str = txn_date.strftime("%Y-%m-%d")
|
||||
elif isinstance(raw_date, date | datetime):
|
||||
date_str = raw_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_str = str(raw_date)
|
||||
|
||||
raw_amount = str(txn.data.get("amount", ""))
|
||||
parts = raw_amount.strip().split()
|
||||
amount_value = float(parts[0]) if parts else 0.0
|
||||
|
||||
deposit = amount_value if amount_value > 0 else ""
|
||||
withdrawal = abs(amount_value) if amount_value < 0 else ""
|
||||
description = txn.data.get("extra_details") or ""
|
||||
reference = txn.data.get("transaction_reference") or ""
|
||||
currency = txn.data.get("currency", "")
|
||||
|
||||
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
|
||||
|
||||
# Prepare in-memory CSV for upload
|
||||
csv_content = csv_buffer.getvalue().encode("utf-8")
|
||||
csv_buffer.close()
|
||||
|
||||
filename = f"{frappe.utils.now_datetime().strftime('%Y%m%d%H%M%S')}_converted_mt940.csv"
|
||||
|
||||
# Save to File Manager
|
||||
saved_file = save_file(filename, csv_content, doc.doctype, doc.name, is_private=True, df="import_file")
|
||||
|
||||
return saved_file.file_url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
@@ -232,12 +128,6 @@ def download_import_log(data_import_name):
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def is_mt940_format(content: str) -> bool:
|
||||
"""Check if the content has key MT940 tags"""
|
||||
required_tags = [":20:", ":25:", ":28C:", ":61:"]
|
||||
return all(tag in content for tag in required_tags)
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
@@ -284,7 +174,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""Update bank transaction mapping database with template options."""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -296,7 +185,6 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""Add bank account information to data rows."""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -313,7 +201,6 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""Write processed data to CSV or Excel files."""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -323,12 +210,11 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension in ("xlsx", "xls"):
|
||||
elif extension == "xlsx" or "xls":
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""Write data to Excel file with formatting."""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
@@ -372,7 +258,7 @@ def get_import_status(docname):
|
||||
|
||||
logs = frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=[{"COUNT": "*", "as": "count"}, "success"],
|
||||
fields=["count(*) as count", "success"],
|
||||
filters={"data_import": docname},
|
||||
group_by="success",
|
||||
)
|
||||
@@ -393,7 +279,7 @@ def get_import_status(docname):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_logs(docname: str):
|
||||
frappe.has_permission("Bank Statement Import", throw=True)
|
||||
frappe.has_permission("Bank Statement Import")
|
||||
|
||||
return frappe.get_all(
|
||||
"Data Import Log",
|
||||
|
||||
@@ -1,209 +1,10 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""Test boundary conditions for statement number length"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""Test with real-world MT940 content that was failing in production"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
from rapidfuzz.utils import default_process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
@@ -26,7 +25,7 @@ class AutoMatchParty:
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
fuzzy_matching_enabled = frappe.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
if not result and fuzzy_matching_enabled:
|
||||
result = AutoMatchbyPartyNameDescription(
|
||||
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
|
||||
@@ -133,7 +132,6 @@ class AutoMatchbyPartyNameDescription:
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
processor=default_process,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
|
||||
@@ -2,21 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bank Transaction", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("party_type", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: { is_company_account: 1 },
|
||||
};
|
||||
});
|
||||
|
||||
onload(frm) {
|
||||
frm.set_query("payment_document", "payment_entries", function () {
|
||||
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
||||
return {
|
||||
@@ -25,16 +11,7 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_entry", "payment_entries", function () {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ["!=", 2],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||
@@ -42,17 +19,51 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
set_bank_statement_filter(frm);
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.set_query("party_type", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_payment_doctypes: function () {
|
||||
// get payment doctypes from all the apps
|
||||
return ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Bank Transaction"];
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Bank Transaction Payments", {
|
||||
payment_entries_remove: function (frm, cdt, cdn) {
|
||||
update_clearance_date(frm, cdt, cdn);
|
||||
},
|
||||
});
|
||||
|
||||
const update_clearance_date = (frm, cdt, cdn) => {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frappe
|
||||
.xcall("erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", {
|
||||
doctype: cdt,
|
||||
docname: cdn,
|
||||
bt_name: frm.doc.name,
|
||||
})
|
||||
.then((e) => {
|
||||
if (e == "success") {
|
||||
frappe.show_alert({
|
||||
message: __("Document {0} successfully uncleared", [e]),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function set_bank_statement_filter(frm) {
|
||||
frm.set_query("bank_statement", function () {
|
||||
return {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"default": "ACC-BTN-.YYYY.-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "ACC-BTN-.YYYY.-",
|
||||
@@ -116,14 +117,15 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction ID",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -222,8 +224,7 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -235,10 +236,9 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -287,10 +287,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, getdate
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
class BankTransaction(Document):
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.SmallText | None
|
||||
reference_number: DF.Data | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
@@ -84,16 +84,16 @@ class BankTransaction(Document):
|
||||
if not self.payment_entries:
|
||||
return
|
||||
|
||||
references = set()
|
||||
pe = []
|
||||
for row in self.payment_entries:
|
||||
reference = (row.payment_document, row.payment_entry)
|
||||
if reference in references:
|
||||
if reference in pe:
|
||||
frappe.throw(
|
||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||
row.payment_document, row.payment_entry
|
||||
)
|
||||
)
|
||||
references.add(reference)
|
||||
pe.append(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
allocated_amount = (
|
||||
@@ -104,36 +104,22 @@ class BankTransaction(Document):
|
||||
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
|
||||
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
|
||||
|
||||
def delink_old_payment_entries(self):
|
||||
if self.flags.updating_linked_bank_transaction:
|
||||
return
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
payment_entry_names = set(pe.name for pe in self.payment_entries)
|
||||
|
||||
for old_pe in old_doc.payment_entries:
|
||||
if old_pe.name in payment_entry_names:
|
||||
continue
|
||||
|
||||
self.delink_payment_entry(old_pe)
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_duplicate_references()
|
||||
self.update_allocated_amount()
|
||||
self.delink_old_payment_entries()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.delink_payment_entry(payment_entry)
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
|
||||
self.set_status()
|
||||
|
||||
@@ -166,55 +152,43 @@ class BankTransaction(Document):
|
||||
- 0 > a: Error: already over-allocated
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
if self.flags.updating_linked_bank_transaction or not self.payment_entries:
|
||||
return
|
||||
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
gl_entries = get_related_bank_gl_entries(payment_entry_docs)
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||
|
||||
for payment_entry in list(self.payment_entries):
|
||||
if payment_entry.allocated_amount != 0:
|
||||
continue
|
||||
|
||||
allocable_amount, should_clear, clearance_date = get_clearance_details(
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||
gl_entries.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||
gl_bank_account,
|
||||
)
|
||||
|
||||
if allocable_amount < 0:
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(allocable_amount))
|
||||
|
||||
if remaining_amount <= 0:
|
||||
self.remove(payment_entry)
|
||||
continue
|
||||
|
||||
if allocable_amount == 0:
|
||||
if should_clear:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||
self.remove(payment_entry)
|
||||
continue
|
||||
|
||||
should_clear = should_clear and allocable_amount <= remaining_amount
|
||||
payment_entry.allocated_amount = min(allocable_amount, remaining_amount)
|
||||
remaining_amount = flt(
|
||||
remaining_amount - payment_entry.allocated_amount,
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
self.update_linked_bank_transaction(
|
||||
payment_entry.payment_entry, payment_entry.allocated_amount
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
)
|
||||
elif should_clear:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||
|
||||
self.update_allocated_amount()
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
||||
payment_entry.allocated_amount = unallocated_amount
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount:
|
||||
payment_entry.allocated_amount = remaining_amount
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
for payment_entry in to_remove:
|
||||
self.remove(payment_entry)
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
@@ -225,64 +199,14 @@ class BankTransaction(Document):
|
||||
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.delink_payment_entry(payment_entry)
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.remove(payment_entry)
|
||||
|
||||
def delink_payment_entry(self, payment_entry):
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
self.update_linked_bank_transaction(payment_entry.payment_entry, allocated_amount=None)
|
||||
else:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=None)
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, clearance_date=None):
|
||||
doctype = payment_entry.payment_document
|
||||
docname = payment_entry.payment_entry
|
||||
|
||||
# might be a bank transaction
|
||||
if doctype not in get_doctypes_for_bank_reconciliation():
|
||||
return
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
def update_linked_bank_transaction(self, bank_transaction_name, allocated_amount=None):
|
||||
"""For when a second bank transaction has fixed another, e.g. refund"""
|
||||
|
||||
bt = frappe.get_doc(self.doctype, bank_transaction_name)
|
||||
if allocated_amount:
|
||||
bt.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": self.doctype,
|
||||
"payment_entry": self.name,
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
pe = next(
|
||||
(
|
||||
pe
|
||||
for pe in bt.payment_entries
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not pe:
|
||||
return
|
||||
|
||||
bt.flags.updating_linked_bank_transaction = True
|
||||
bt.remove(pe)
|
||||
|
||||
bt.save()
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||
)
|
||||
|
||||
def auto_set_party(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
||||
@@ -314,107 +238,71 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries, gl_bank_account):
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
There should only be one bank gl entry for a voucher, except for JE.
|
||||
For JE, there can be multiple bank gl entries for the same account.
|
||||
In this case, the allocable_amount will be the sum of amounts of all gl entries of the account.
|
||||
There will be no gl entry for a Bank Transaction so return the unallocated amount.
|
||||
Should only clear the voucher if all bank gl entries are allocated.
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
But if a JE, could affect two banks.
|
||||
Should only clear the voucher if all bank gles are allocated.
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
transaction_date = getdate(transaction.date)
|
||||
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
bt = frappe.db.get_value(
|
||||
"Bank Transaction",
|
||||
payment_entry.payment_entry,
|
||||
("unallocated_amount", "bank_account"),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if bt.bank_account != gl_bank_account:
|
||||
frappe.throw(
|
||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
||||
)
|
||||
unmatched_gles = len(gles)
|
||||
latest_transaction = transaction
|
||||
for gle in gles:
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
||||
)
|
||||
)
|
||||
|
||||
return abs(bt.unallocated_amount), True, transaction_date
|
||||
unmatched_gles -= 1
|
||||
unallocated_amount = gle["amount"]
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"]:
|
||||
unallocated_amount = gle["amount"] - a["total"]
|
||||
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
|
||||
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
|
||||
else:
|
||||
# Must be a Journal Entry affecting more than one bank
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
|
||||
unmatched_gles -= 1
|
||||
|
||||
if gl_bank_account not in gl_entries:
|
||||
frappe.throw(
|
||||
_("{} {} is not affecting bank account {}").format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
allocable_amount = gl_entries.pop(gl_bank_account) or 0
|
||||
if allocable_amount <= 0.0:
|
||||
frappe.throw(
|
||||
_("Invalid amount in accounting entries of {} {} for Account {}: {}").format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account, allocable_amount
|
||||
)
|
||||
)
|
||||
|
||||
matching_bt_allocaion = bt_allocations.pop(gl_bank_account, {})
|
||||
|
||||
allocable_amount = flt(
|
||||
allocable_amount - matching_bt_allocaion.get("total", 0), transaction.precision("unallocated_amount")
|
||||
)
|
||||
|
||||
should_clear = all(
|
||||
gl_entries[gle_account] == bt_allocations.get(gle_account, {}).get("total", 0)
|
||||
for gle_account in gl_entries
|
||||
)
|
||||
|
||||
bt_allocation_date = matching_bt_allocaion.get("latest_date", None)
|
||||
clearance_date = transaction_date if not bt_allocation_date else max(transaction_date, bt_allocation_date)
|
||||
|
||||
return allocable_amount, should_clear, clearance_date
|
||||
return unallocated_amount, unmatched_gles == 0, latest_transaction
|
||||
|
||||
|
||||
def get_related_bank_gl_entries(docs):
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
result = frappe.db.sql(
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
gle.voucher_type AS doctype,
|
||||
gle.voucher_no AS docname,
|
||||
gle.account AS gl_account,
|
||||
SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
|
||||
FROM
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabAccount` ac ON ac.name = gle.account
|
||||
WHERE
|
||||
ac.account_type = 'Bank'
|
||||
AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
|
||||
AND gle.is_cancelled = 0
|
||||
GROUP BY
|
||||
gle.voucher_type, gle.voucher_no, gle.account
|
||||
""",
|
||||
{"docs": docs},
|
||||
SELECT
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
gle.account AS gl_account
|
||||
FROM
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabAccount` ac ON ac.name=gle.account
|
||||
WHERE
|
||||
ac.account_type = 'Bank'
|
||||
AND gle.voucher_type = %(doctype)s
|
||||
AND gle.voucher_no = %(docname)s
|
||||
AND is_cancelled = 0
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
entries = {}
|
||||
for row in result:
|
||||
key = (row["doctype"], row["docname"])
|
||||
if key not in entries:
|
||||
entries[key] = {}
|
||||
entries[key][row["gl_account"]] = row["amount"]
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction date
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
@@ -423,10 +311,11 @@ def get_total_allocated_amount(docs):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
@@ -449,14 +338,104 @@ def get_total_allocated_amount(docs):
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
row["latest_date"] = getdate(row["latest_date"])
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), {})[
|
||||
row["gl_account"]
|
||||
] = row
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
paid_amount_field = "paid_amount"
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
|
||||
if doc.payment_type == "Receive":
|
||||
paid_amount_field = (
|
||||
"received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
|
||||
)
|
||||
elif doc.payment_type == "Pay":
|
||||
paid_amount_field = (
|
||||
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return abs(
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(debit_in_account_currency-credit_in_account_currency)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Disbursement":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Repayment":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
|
||||
|
||||
elif payment_entry.payment_document == "Bank Transaction":
|
||||
dep, wth = frappe.db.get_value(
|
||||
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
|
||||
)
|
||||
return abs(flt(wth) - flt(dep))
|
||||
|
||||
else:
|
||||
frappe.throw(
|
||||
f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually"
|
||||
)
|
||||
|
||||
|
||||
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if doctype in get_doctypes_for_bank_reconciliation():
|
||||
if (
|
||||
doctype == "Payment Entry"
|
||||
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||
):
|
||||
return
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Bank Transaction":
|
||||
# For when a second bank transaction has fixed another, e.g. refund
|
||||
bt = frappe.get_doc(doctype, docname)
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
bt.save()
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
bt.remove(pe)
|
||||
bt.save()
|
||||
break
|
||||
|
||||
|
||||
def get_reconciled_bank_transactions(doctype, docname):
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
@@ -465,6 +444,13 @@ def get_reconciled_bank_transactions(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unclear_reference_payment(doctype, docname, bt_name):
|
||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||
set_voucher_clearance(doctype, docname, None, bt)
|
||||
return docname
|
||||
|
||||
|
||||
def remove_from_bank_transaction(doctype, docname):
|
||||
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
||||
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
IBAN_1 = "DE02000000003716541159"
|
||||
IBAN_2 = "DE02500105170137075030"
|
||||
|
||||
class UnitTestBankTransaction(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BankTransaction.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestAutoMatchParty(IntegrationTestCase):
|
||||
@@ -25,24 +31,24 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban=IBAN_1)
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
@@ -54,7 +60,7 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban=IBAN_2,
|
||||
iban="DE04000000003716545346",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
@@ -6,15 +6,12 @@ import json
|
||||
import frappe
|
||||
from frappe import utils
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
get_linked_payments,
|
||||
reconcile_vouchers,
|
||||
)
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -24,6 +21,15 @@ from erpnext.tests.utils import if_lending_app_installed
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item", "Cost Center"]
|
||||
|
||||
|
||||
class UnitTestBankTransaction(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BankTransaction.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBankTransaction(IntegrationTestCase):
|
||||
def setUp(self):
|
||||
make_pos_profile()
|
||||
@@ -424,13 +430,15 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Wire Transfer"})
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", gl_account)
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 109080})
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestBisectAccountingStatements(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BisectAccountingStatements.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBisectAccountingStatements(IntegrationTestCase):
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestBisectNodes(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BisectNodes.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBisectNodes(IntegrationTestCase):
|
||||
|
||||
@@ -4,6 +4,16 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
frappe.ui.form.on("Budget", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("account", "accounts", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("monthly_distribution", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -13,35 +23,10 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: async function (frm) {
|
||||
refresh: function (frm) {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
|
||||
let exception_role = await frappe.db.get_value(
|
||||
"Company",
|
||||
frm.doc.company,
|
||||
"exception_budget_approver_role"
|
||||
);
|
||||
|
||||
const role = exception_role.message.exception_budget_approver_role;
|
||||
|
||||
if (role && frappe.user.has_role(role)) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
budget_against: function (frm) {
|
||||
@@ -49,15 +34,6 @@ frappe.ui.form.on("Budget", {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
},
|
||||
|
||||
budget_amount(frm) {
|
||||
if (frm.doc.budget_distribution?.length) {
|
||||
frm.doc.budget_distribution.forEach((row) => {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
});
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
|
||||
set_null_value: function (frm) {
|
||||
if (frm.doc.budget_against == "Cost Center") {
|
||||
frm.set_value("project", null);
|
||||
@@ -70,44 +46,4 @@ frappe.ui.form.on("Budget", {
|
||||
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
|
||||
frm.toggle_reqd("project", frm.doc.budget_against == "Project");
|
||||
},
|
||||
|
||||
revise_budget_action: function (frm) {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created."
|
||||
),
|
||||
function () {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.budget.budget.revise_budget",
|
||||
args: { budget_name: frm.doc.name },
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__("New revised budget created successfully"));
|
||||
frappe.set_route("Form", "Budget", r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
function () {
|
||||
frappe.msgprint(__("Revision cancelled"));
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Budget Distribution", {
|
||||
amount(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
percent(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,24 +7,15 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"account",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
"monthly_distribution",
|
||||
"amended_from",
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_start_date",
|
||||
"budget_end_date",
|
||||
"distribution_frequency",
|
||||
"budget_amount",
|
||||
"section_break_nwug",
|
||||
"distribute_equally",
|
||||
"section_break_fpdt",
|
||||
"budget_distribution",
|
||||
"section_break_6",
|
||||
"applicable_on_material_request",
|
||||
"action_if_annual_budget_exceeded_on_mr",
|
||||
@@ -37,12 +28,8 @@
|
||||
"applicable_on_booking_actual_expenses",
|
||||
"action_if_annual_budget_exceeded",
|
||||
"action_if_accumulated_monthly_budget_exceeded",
|
||||
"control_action_for_cumulative_expense_section",
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_kkan",
|
||||
"revision_of"
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -53,7 +40,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Budget Against",
|
||||
"options": "\nCost Center\nProject",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -63,7 +49,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -73,8 +58,7 @@
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.budget_against == 'Project'",
|
||||
@@ -82,13 +66,28 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
|
||||
"fieldname": "monthly_distribution",
|
||||
"fieldtype": "Link",
|
||||
"label": "Monthly Distribution",
|
||||
"options": "Monthly Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -184,132 +183,31 @@
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"default": "BUDGET-.########",
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Accounts",
|
||||
"options": "Budget Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.########",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"read_only": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "control_action_for_cumulative_expense_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Control Action for Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "(Purchase Order + Material Request + Actual Expense)",
|
||||
"fieldname": "applicable_on_cumulative_expense",
|
||||
"fieldtype": "Check",
|
||||
"label": "Applicable on Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_annual_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Anual Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fpdt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Distribution",
|
||||
"options": "Budget Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_kkan",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "revision_of",
|
||||
"fieldtype": "Data",
|
||||
"label": "Revision Of",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "distribute_equally",
|
||||
"fieldtype": "Check",
|
||||
"label": "Distribute Equally"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_nwug",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "To Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_start_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_end_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget End Date"
|
||||
},
|
||||
{
|
||||
"default": "Monthly",
|
||||
"fieldname": "distribution_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Distribution Frequency",
|
||||
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-19 17:00:00.648224",
|
||||
"modified": "2024-03-27 13:06:42.675933",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
@@ -333,9 +231,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
|
||||
from frappe.utils.data import get_first_day, nowdate
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -34,134 +30,87 @@ class Budget(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
|
||||
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount
|
||||
|
||||
account: DF.Link
|
||||
accounts: DF.Table[BudgetAccount]
|
||||
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
amended_from: DF.Link | None
|
||||
applicable_on_booking_actual_expenses: DF.Check
|
||||
applicable_on_cumulative_expense: DF.Check
|
||||
applicable_on_material_request: DF.Check
|
||||
applicable_on_purchase_order: DF.Check
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
budget_amount: DF.Currency
|
||||
budget_distribution: DF.Table[BudgetDistribution]
|
||||
budget_end_date: DF.Date | None
|
||||
budget_start_date: DF.Date | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
distribute_equally: DF.Check
|
||||
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
|
||||
from_fiscal_year: DF.Link
|
||||
naming_series: DF.Literal["BUDGET-.########"]
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Data | None
|
||||
project: DF.Link | None
|
||||
revision_of: DF.Data | None
|
||||
to_fiscal_year: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
if not self.get(frappe.scrub(self.budget_against)):
|
||||
frappe.throw(_("{0} is mandatory").format(self.budget_against))
|
||||
self.validate_budget_amount()
|
||||
self.validate_fiscal_year()
|
||||
self.set_fiscal_year_dates()
|
||||
self.validate_duplicate()
|
||||
self.validate_account()
|
||||
self.validate_accounts()
|
||||
self.set_null_value()
|
||||
self.validate_applicable_for()
|
||||
self.validate_existing_expenses()
|
||||
|
||||
def validate_budget_amount(self):
|
||||
if self.budget_amount <= 0:
|
||||
frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount))
|
||||
|
||||
def validate_fiscal_year(self):
|
||||
if self.from_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.from_fiscal_year, self.company)
|
||||
if self.to_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.to_fiscal_year, self.company)
|
||||
|
||||
def validate_fiscal_year_company(self, fiscal_year, company):
|
||||
linked_companies = frappe.get_all(
|
||||
"Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company"
|
||||
)
|
||||
if linked_companies and company not in linked_companies:
|
||||
frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company))
|
||||
|
||||
def set_fiscal_year_dates(self):
|
||||
if self.from_fiscal_year:
|
||||
self.budget_start_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.from_fiscal_year, "year_start_date"
|
||||
)
|
||||
if self.to_fiscal_year:
|
||||
self.budget_end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.to_fiscal_year, "year_end_date"
|
||||
)
|
||||
|
||||
if self.budget_start_date > self.budget_end_date:
|
||||
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
|
||||
|
||||
def validate_duplicate(self):
|
||||
budget_against_field = frappe.scrub(self.budget_against)
|
||||
budget_against = self.get(budget_against_field)
|
||||
account = self.account
|
||||
|
||||
if not account:
|
||||
return
|
||||
|
||||
accounts = [d.account for d in self.accounts] or []
|
||||
existing_budget = frappe.db.sql(
|
||||
f"""
|
||||
SELECT name, account
|
||||
FROM `tabBudget`
|
||||
WHERE
|
||||
docstatus < 2
|
||||
AND company = %s
|
||||
AND {budget_against_field} = %s
|
||||
AND account = %s
|
||||
AND name != %s
|
||||
AND (
|
||||
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
)
|
||||
""",
|
||||
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
|
||||
as_dict=True,
|
||||
"""
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
|
||||
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
|
||||
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
|
||||
),
|
||||
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if existing_budget:
|
||||
d = existing_budget[0]
|
||||
for d in existing_budget:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years."
|
||||
).format(d.name, self.budget_against, budget_against, d.account),
|
||||
"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_account(self):
|
||||
if not self.account:
|
||||
frappe.throw(_("Account is mandatory"))
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
self.account
|
||||
def validate_accounts(self):
|
||||
account_list = []
|
||||
for d in self.get("accounts"):
|
||||
if d.account:
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
|
||||
else:
|
||||
account_list.append(d.account)
|
||||
|
||||
def set_null_value(self):
|
||||
if self.budget_against == "Cost Center":
|
||||
@@ -187,201 +136,31 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def validate_existing_expenses(self):
|
||||
if self.is_new() and self.revision_of:
|
||||
return
|
||||
|
||||
params = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"account": self.account,
|
||||
"budget_start_date": self.budget_start_date,
|
||||
"budget_end_date": self.budget_end_date,
|
||||
"budget_against_field": frappe.scrub(self.budget_against),
|
||||
"budget_against_doctype": frappe.unscrub(self.budget_against),
|
||||
}
|
||||
)
|
||||
|
||||
params[params.budget_against_field] = self.get(params.budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"):
|
||||
params.is_tree = True
|
||||
else:
|
||||
params.is_tree = False
|
||||
|
||||
actual_spent = get_actual_expense(params)
|
||||
|
||||
if actual_spent > self.budget_amount:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Spending for Account {0} ({1}) between {2} and {3} "
|
||||
"has already exceeded the new allocated budget. "
|
||||
"Spent: {4}, Budget: {5}"
|
||||
).format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
frappe.bold(self.budget_start_date),
|
||||
frappe.bold(self.budget_end_date),
|
||||
frappe.bold(frappe.utils.fmt_money(actual_spent)),
|
||||
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
|
||||
),
|
||||
title=_("Budget Limit Exceeded"),
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.allocate_budget()
|
||||
|
||||
def on_update(self):
|
||||
self.validate_distribution_totals()
|
||||
|
||||
def allocate_budget(self):
|
||||
if self.revision_of:
|
||||
return
|
||||
|
||||
if not self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
self.set("budget_distribution", [])
|
||||
|
||||
periods = self.get_budget_periods()
|
||||
total_periods = len(periods)
|
||||
row_percent = 100 / total_periods if total_periods else 0
|
||||
|
||||
for start_date, end_date in periods:
|
||||
row = self.append("budget_distribution", {})
|
||||
row.start_date = start_date
|
||||
row.end_date = end_date
|
||||
self.add_allocated_amount(row, row_percent)
|
||||
|
||||
def should_regenerate_budget_distribution(self):
|
||||
"""Check whether budget distribution should be recalculated."""
|
||||
old_doc = self.get_doc_before_save() if not self.is_new() else None
|
||||
if not old_doc or not self.budget_distribution:
|
||||
return True
|
||||
|
||||
if old_doc:
|
||||
changed_fields = [
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_amount",
|
||||
"distribution_frequency",
|
||||
"distribute_equally",
|
||||
]
|
||||
for field in changed_fields:
|
||||
if old_doc.get(field) != self.get(field):
|
||||
return True
|
||||
|
||||
return bool(self.distribute_equally)
|
||||
|
||||
def get_budget_periods(self):
|
||||
"""Return list of (start_date, end_date) tuples based on frequency."""
|
||||
frequency = self.distribution_frequency
|
||||
periods = []
|
||||
|
||||
start_date = getdate(self.budget_start_date)
|
||||
end_date = getdate(self.budget_end_date)
|
||||
|
||||
while start_date <= end_date:
|
||||
period_start = get_first_day(start_date)
|
||||
period_end = self.get_period_end(period_start, frequency)
|
||||
period_end = min(period_end, end_date)
|
||||
|
||||
periods.append((period_start, period_end))
|
||||
start_date = add_months(period_start, self.get_month_increment(frequency))
|
||||
|
||||
return periods
|
||||
|
||||
def get_period_end(self, start_date, frequency):
|
||||
"""Return the correct end date for a given frequency."""
|
||||
if frequency == "Monthly":
|
||||
return get_last_day(start_date)
|
||||
elif frequency == "Quarterly":
|
||||
return get_last_day(add_months(start_date, 2))
|
||||
elif frequency == "Half-Yearly":
|
||||
return get_last_day(add_months(start_date, 5))
|
||||
else: # Yearly
|
||||
return get_last_day(add_months(start_date, 11))
|
||||
|
||||
def get_month_increment(self, frequency):
|
||||
"""Return how many months to move forward for the next period."""
|
||||
return {
|
||||
"Monthly": 1,
|
||||
"Quarterly": 3,
|
||||
"Half-Yearly": 6,
|
||||
"Yearly": 12,
|
||||
}.get(frequency, 1)
|
||||
|
||||
def add_allocated_amount(self, row, row_percent):
|
||||
if not self.distribute_equally:
|
||||
row.amount = 0
|
||||
row.percent = 0
|
||||
else:
|
||||
row.amount = flt(self.budget_amount * row_percent / 100, 3)
|
||||
row.percent = flt(row_percent, 3)
|
||||
|
||||
def validate_distribution_totals(self):
|
||||
if self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
total_amount = sum(d.amount for d in self.budget_distribution)
|
||||
total_percent = sum(d.percent for d in self.budget_distribution)
|
||||
|
||||
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
|
||||
frappe.throw(
|
||||
_("Total distributed amount {0} must be equal to Budget Amount {1}").format(
|
||||
flt(total_amount, 2), self.budget_amount
|
||||
)
|
||||
)
|
||||
|
||||
if flt(abs(total_percent - 100), 2) > 0.10:
|
||||
frappe.throw(
|
||||
_("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2))
|
||||
)
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
|
||||
def validate_expense_against_budget(params, expense_amount=0):
|
||||
params = frappe._dict(params)
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
if not frappe.get_all("Budget", limit=1):
|
||||
return
|
||||
|
||||
if not params.fiscal_year:
|
||||
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
|
||||
|
||||
posting_date = getdate(params.get("posting_date"))
|
||||
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
|
||||
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
|
||||
|
||||
budget_exists = frappe.db.sql(
|
||||
"""
|
||||
select name
|
||||
from `tabBudget`
|
||||
where company = %s
|
||||
and docstatus = 1
|
||||
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
limit 1
|
||||
""",
|
||||
(params.company, year_end_date, year_start_date),
|
||||
)
|
||||
|
||||
if not budget_exists:
|
||||
return
|
||||
|
||||
if params.get("company"):
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", params.get("company"), "exception_budget_approver_role"
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not params.account:
|
||||
params.account = params.get("expense_account")
|
||||
if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec
|
||||
return
|
||||
|
||||
if not params.get("expense_account") and params.get("account"):
|
||||
params.expense_account = params.account
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
|
||||
if not (params.get("account") and params.get("cost_center")) and params.item_code:
|
||||
params.cost_center, params.account = get_item_details(params)
|
||||
if not (args.get("account") and args.get("cost_center")) and args.item_code:
|
||||
args.cost_center, args.account = get_item_details(args)
|
||||
|
||||
if not params.account:
|
||||
if not args.account:
|
||||
return
|
||||
|
||||
default_dimensions = [
|
||||
@@ -399,78 +178,59 @@ def validate_expense_against_budget(params, expense_amount=0):
|
||||
budget_against = dimension.get("fieldname")
|
||||
|
||||
if (
|
||||
params.get(budget_against)
|
||||
and params.account
|
||||
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||
):
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
|
||||
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
params.is_tree = True
|
||||
args.is_tree = True
|
||||
else:
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
|
||||
params.is_tree = False
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
|
||||
args.is_tree = False
|
||||
|
||||
params.budget_against_field = budget_against
|
||||
params.budget_against_doctype = doctype
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
b.name,
|
||||
b.{budget_against} AS budget_against,
|
||||
b.budget_amount,
|
||||
b.from_fiscal_year,
|
||||
b.to_fiscal_year,
|
||||
b.budget_start_date,
|
||||
b.budget_end_date,
|
||||
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
|
||||
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
|
||||
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded,
|
||||
b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
FROM
|
||||
`tabBudget` b
|
||||
WHERE
|
||||
b.company = %s
|
||||
AND b.docstatus = 1
|
||||
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
|
||||
AND b.account = %s
|
||||
select
|
||||
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
from
|
||||
`tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
{condition}
|
||||
""",
|
||||
(params.company, params.posting_date, params.account),
|
||||
""",
|
||||
(args.fiscal_year, args.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
if budget_records:
|
||||
validate_budget_records(params, budget_records, expense_amount)
|
||||
validate_budget_records(args, budget_records, expense_amount)
|
||||
|
||||
|
||||
def validate_budget_records(params, budget_records, expense_amount):
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
yearly_action, monthly_action = get_actions(params, budget)
|
||||
params["for_material_request"] = budget.for_material_request
|
||||
params["for_purchase_order"] = budget.for_purchase_order
|
||||
params["from_fiscal_year"], params["to_fiscal_year"] = (
|
||||
budget.from_fiscal_year,
|
||||
budget.to_fiscal_year,
|
||||
)
|
||||
params["budget_start_date"], params["budget_end_date"] = (
|
||||
budget.budget_start_date,
|
||||
budget.budget_end_date,
|
||||
)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
args["for_material_request"] = budget.for_material_request
|
||||
args["for_purchase_order"] = budget.for_purchase_order
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
params,
|
||||
args,
|
||||
flt(budget.budget_amount),
|
||||
_("Annual"),
|
||||
yearly_action,
|
||||
@@ -479,12 +239,14 @@ def validate_budget_records(params, budget_records, expense_amount):
|
||||
)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
|
||||
budget_amount = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
|
||||
)
|
||||
|
||||
params["month_end_date"] = get_last_day(params.posting_date)
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
|
||||
compare_expense_with_budget(
|
||||
params,
|
||||
args,
|
||||
budget_amount,
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
@@ -493,41 +255,40 @@ def validate_budget_records(params, budget_records, expense_amount):
|
||||
)
|
||||
|
||||
|
||||
def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0):
|
||||
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||
if not amount:
|
||||
params.requested_amount, params.ordered_amount = (
|
||||
get_requested_amount(params),
|
||||
get_ordered_amount(params),
|
||||
)
|
||||
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||
|
||||
if params.get("doctype") == "Material Request" and params.for_material_request:
|
||||
amount = params.requested_amount + params.ordered_amount
|
||||
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||
amount = args.requested_amount + args.ordered_amount
|
||||
|
||||
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
|
||||
amount = params.ordered_amount
|
||||
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||
amount = args.ordered_amount
|
||||
|
||||
total_expense = params.actual_expense + amount
|
||||
total_expense = args.actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if params.actual_expense > budget_amount:
|
||||
diff = params.actual_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
|
||||
if args.actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = args.actual_expense - budget_amount
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
|
||||
|
||||
currency = frappe.get_cached_value("Company", params.company, "default_currency")
|
||||
msg = _msg.format(
|
||||
currency = frappe.get_cached_value("Company", args.company, "default_currency")
|
||||
|
||||
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
|
||||
_(action_for),
|
||||
frappe.bold(params.account),
|
||||
frappe.unscrub(params.budget_against_field),
|
||||
frappe.bold(args.account),
|
||||
frappe.unscrub(args.budget_against_field),
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
error_tense,
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
msg += get_expense_breakup(params, currency, budget_against)
|
||||
msg += get_expense_breakup(args, currency, budget_against)
|
||||
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
@@ -540,25 +301,14 @@ def compare_expense_with_budget(params, budget_amount, action_for, action, budge
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(params, currency, budget_against):
|
||||
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
params.budget_against_field: budget_against,
|
||||
"account": params.account,
|
||||
"company": params.company,
|
||||
}
|
||||
)
|
||||
|
||||
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
gl_filters = common_filters.copy()
|
||||
gl_filters.update(
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"is_cancelled": 0,
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -566,85 +316,86 @@ def get_expense_breakup(params, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label=_("Actual Expenses"),
|
||||
filters=gl_filters,
|
||||
label="Actual Expenses",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(params.actual_expense, currency=currency))
|
||||
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
mr_filters = common_filters.copy()
|
||||
mr_filters.update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label=_("Material Requests"),
|
||||
label="Material Requests",
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=mr_filters,
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(params.requested_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
po_filters = common_filters.copy()
|
||||
po_filters.update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=_("Unbilled Orders"),
|
||||
label="Unbilled Orders",
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=po_filters,
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(params.ordered_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def get_actions(params, budget):
|
||||
def get_actions(args, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
|
||||
if params.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
|
||||
|
||||
elif params.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_po
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_requested_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Material Request")
|
||||
def get_requested_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
@@ -658,9 +409,9 @@ def get_requested_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Purchase Order")
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
@@ -674,102 +425,111 @@ def get_ordered_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(params, for_doc):
|
||||
condition = f"expense_account = '{params.expense_account}'"
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and params.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
start_date, end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
|
||||
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
def get_actual_expense(params):
|
||||
if not params.budget_against_doctype:
|
||||
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
|
||||
def get_actual_expense(args):
|
||||
if not args.budget_against_doctype:
|
||||
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
|
||||
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||
|
||||
date_condition = (
|
||||
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
|
||||
)
|
||||
|
||||
if params.is_tree:
|
||||
if args.is_tree:
|
||||
lft_rgt = frappe.db.get_value(
|
||||
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
params.update(lft_rgt)
|
||||
|
||||
condition2 = f"""
|
||||
and exists(
|
||||
select name from `tab{params.budget_against_doctype}`
|
||||
where lft >= %(lft)s and rgt <= %(rgt)s
|
||||
and name = gle.{budget_against_field}
|
||||
)
|
||||
"""
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})"""
|
||||
else:
|
||||
condition2 = f"""
|
||||
and gle.{budget_against_field} = %({budget_against_field})s
|
||||
"""
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account = %(account)s
|
||||
{condition1}
|
||||
{date_condition}
|
||||
and gle.company = %(company)s
|
||||
and gle.docstatus = 1
|
||||
{condition2}
|
||||
""",
|
||||
params,
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account=%(account)s
|
||||
{condition1}
|
||||
and gle.fiscal_year=%(fiscal_year)s
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""",
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_accumulated_monthly_budget(budget_name, posting_date):
|
||||
posting_date = getdate(posting_date)
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
bd = frappe.qb.DocType("Budget Distribution")
|
||||
b = frappe.qb.DocType("Budget")
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(bd)
|
||||
.join(b)
|
||||
.on(bd.parent == b.name)
|
||||
.select(Sum(bd.amount).as_("accumulated_amount"))
|
||||
.where(b.name == budget_name)
|
||||
.where(bd.start_date <= posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
return flt(result[0]["accumulated_amount"]) if result else 0.0
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution:
|
||||
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
|
||||
else:
|
||||
accumulated_percentage += 100.0 / 12
|
||||
|
||||
dt = add_months(dt, 1)
|
||||
|
||||
return annual_budget * accumulated_percentage / 100
|
||||
|
||||
|
||||
def get_item_details(params):
|
||||
def get_item_details(args):
|
||||
cost_center, expense_account = None, None
|
||||
|
||||
if not params.get("company"):
|
||||
if not args.get("company"):
|
||||
return cost_center, expense_account
|
||||
|
||||
if params.item_code:
|
||||
if args.item_code:
|
||||
item_defaults = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": params.item_code, "company": params.get("company")},
|
||||
{"parent": args.item_code, "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
if item_defaults:
|
||||
@@ -777,7 +537,7 @@ def get_item_details(params):
|
||||
|
||||
if not (cost_center and expense_account):
|
||||
for doctype in ["Item Group", "Company"]:
|
||||
data = get_expense_cost_center(doctype, params)
|
||||
data = get_expense_cost_center(doctype, args)
|
||||
|
||||
if not cost_center and data:
|
||||
cost_center = data[0]
|
||||
@@ -791,39 +551,14 @@ def get_item_details(params):
|
||||
return cost_center, expense_account
|
||||
|
||||
|
||||
def get_expense_cost_center(doctype, params):
|
||||
def get_expense_cost_center(doctype, args):
|
||||
if doctype == "Item Group":
|
||||
return frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
|
||||
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_value(
|
||||
doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
)
|
||||
|
||||
|
||||
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||
from_year = frappe.get_cached_value(
|
||||
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
to_year = frappe.get_cached_value(
|
||||
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
return from_year.year_start_date, to_year.year_end_date
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def revise_budget(budget_name):
|
||||
old_budget = frappe.get_doc("Budget", budget_name)
|
||||
|
||||
if old_budget.docstatus == 1:
|
||||
old_budget.cancel()
|
||||
|
||||
new_budget = frappe.copy_doc(old_budget)
|
||||
new_budget.docstatus = 0
|
||||
new_budget.revision_of = old_budget.name
|
||||
new_budget.insert()
|
||||
|
||||
return new_budget.name
|
||||
|
||||
@@ -3,39 +3,22 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.client import submit
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
revise_budget,
|
||||
)
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Monthly Distribution"]
|
||||
|
||||
|
||||
class TestBudget(ERPNextTestSuite):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.make_monthly_distribution()
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
self.company = "_Test Company"
|
||||
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
|
||||
self.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
self.cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
class TestBudget(IntegrationTestCase):
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -56,18 +39,14 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -80,15 +59,14 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_exception_approver_role(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -112,16 +90,12 @@ class TestBudget(ERPNextTestSuite):
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -135,7 +109,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": accumulated_limit + 1,
|
||||
"rate": 100000,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
@@ -149,26 +123,19 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
mr.cancel()
|
||||
|
||||
def test_monthly_budget_crossed_for_po(self):
|
||||
budget = make_budget(
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
)
|
||||
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
|
||||
|
||||
po.set_missing_values()
|
||||
|
||||
@@ -181,19 +148,16 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
project=project,
|
||||
posting_date=nowdate(),
|
||||
@@ -207,7 +171,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -224,7 +188,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
@@ -244,7 +208,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -273,7 +237,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -305,22 +269,13 @@ class TestBudget(ERPNextTestSuite):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Company - _TC",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center 2 - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -344,19 +299,13 @@ class TestBudget(ERPNextTestSuite):
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
|
||||
)
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
cost_center,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -388,12 +337,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
{"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",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
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",
|
||||
@@ -405,206 +349,6 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
def test_action_for_cumulative_limit(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit - 1,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
jv.submit()
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Stop"
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
)
|
||||
po.set_missing_values()
|
||||
|
||||
self.assertRaises(BudgetError, po.submit)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Ignore"
|
||||
)
|
||||
po.submit()
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
po.cancel()
|
||||
jv.cancel()
|
||||
|
||||
def test_fiscal_year_validation(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2100",
|
||||
"year_start_date": "2100-04-01",
|
||||
"year_end_date": "2101-03-31",
|
||||
"companies": [{"company": "_Test Company"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
from_fiscal_year="2100",
|
||||
to_fiscal_year="2099",
|
||||
do_not_save=True,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_total_distribution_equals_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for row in budget.budget_distribution:
|
||||
row.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_evenly_distribute_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
total = sum([d.amount for d in budget.budget_distribution])
|
||||
self.assertEqual(flt(total), 120000)
|
||||
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
|
||||
|
||||
def test_create_revised_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
self.assertNotEqual(budget.name, revised_budget.name)
|
||||
self.assertEqual(revised_budget.budget_against, budget.budget_against)
|
||||
self.assertEqual(revised_budget.budget_amount, budget.budget_amount)
|
||||
|
||||
old_budget = frappe.get_doc("Budget", budget.name)
|
||||
self.assertEqual(old_budget.docstatus, 2)
|
||||
|
||||
def test_revision_preserves_distribution(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
|
||||
self.assertGreater(len(revised_budget.budget_distribution), 0)
|
||||
|
||||
total = sum(row.amount for row in revised_budget.budget_distribution)
|
||||
self.assertEqual(total, revised_budget.budget_amount)
|
||||
|
||||
def test_manual_budget_amount_total(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=0,
|
||||
budget_amount=30000,
|
||||
budget_start_date="2025-04-01",
|
||||
budget_end_date="2025-06-30",
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
budget.budget_distribution = []
|
||||
|
||||
for row in [
|
||||
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
|
||||
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
|
||||
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
|
||||
]:
|
||||
budget.append("budget_distribution", row)
|
||||
|
||||
budget.save()
|
||||
|
||||
total_child_amount = sum(row.amount for row in budget.budget_distribution)
|
||||
|
||||
self.assertEqual(total_child_amount, budget.budget_amount)
|
||||
|
||||
def test_fiscal_year_company_mismatch(self):
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=True, submit_budget=False)
|
||||
|
||||
fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2099",
|
||||
"year_start_date": "2099-04-01",
|
||||
"year_end_date": "2100-03-31",
|
||||
"companies": [{"company": "_Test Company 2"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget.from_fiscal_year = fy.name
|
||||
budget.to_fiscal_year = fy.name
|
||||
budget.company = "_Test Company"
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_manual_distribution_total_equals_budget_amount(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Cost Center - _TC",
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for d in budget.budget_distribution:
|
||||
d.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_duplicate_budget_validation(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=1,
|
||||
budget_amount=15000,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
new_budget = frappe.new_doc("Budget")
|
||||
new_budget.company = "_Test Company"
|
||||
new_budget.from_fiscal_year = budget.from_fiscal_year
|
||||
new_budget.to_fiscal_year = new_budget.from_fiscal_year
|
||||
new_budget.budget_against = "Cost Center"
|
||||
new_budget.cost_center = "_Test Cost Center - _TC"
|
||||
new_budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
new_budget.budget_amount = 10000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
new_budget.insert()
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
@@ -613,32 +357,21 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
||||
budget_against = budget_against_CC or "_Test Cost Center - _TC"
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
|
||||
|
||||
args = frappe._dict(
|
||||
{
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"month_end_date": posting_date,
|
||||
"monthly_end_date": posting_date,
|
||||
"company": "_Test Company",
|
||||
"from_fiscal_year": fiscal_year,
|
||||
"to_fiscal_year": fiscal_year,
|
||||
"fiscal_year": fiscal_year,
|
||||
"budget_against_field": budget_against_field,
|
||||
"budget_start_date": fiscal_year_start_date,
|
||||
"budget_end_date": fiscal_year_end_date,
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get(budget_against_field):
|
||||
args[budget_against_field] = budget_against
|
||||
|
||||
args.budget_against_doctype = frappe.unscrub(budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
|
||||
args.is_tree = True
|
||||
else:
|
||||
args.is_tree = False
|
||||
|
||||
existing_expense = get_actual_expense(args)
|
||||
|
||||
if existing_expense:
|
||||
@@ -668,33 +401,18 @@ def make_budget(**args):
|
||||
|
||||
budget_against = args.budget_against
|
||||
cost_center = args.cost_center
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"project": project,
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
project_name = "{}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
else:
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"cost_center": cost_center or "_Test Cost Center - _TC",
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for name in budget_list:
|
||||
doc = frappe.get_doc("Budget", name)
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
frappe.delete_doc("Budget", name, force=True, ignore_missing=True)
|
||||
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
|
||||
budget = frappe.new_doc("Budget")
|
||||
|
||||
@@ -703,18 +421,17 @@ def make_budget(**args):
|
||||
else:
|
||||
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
||||
|
||||
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
|
||||
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
|
||||
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
|
||||
monthly_distribution.fiscal_year = fiscal_year
|
||||
|
||||
budget.fiscal_year = fiscal_year
|
||||
budget.monthly_distribution = "_Test Distribution"
|
||||
budget.company = "_Test Company"
|
||||
budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
budget.budget_amount = args.budget_amount or 200000
|
||||
budget.applicable_on_booking_actual_expenses = 1
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
|
||||
budget.distribution_frequency = "Monthly"
|
||||
budget.distribute_equally = args.get("distribute_equally", 1)
|
||||
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
@@ -730,22 +447,7 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_cumulative_expense:
|
||||
budget.applicable_on_cumulative_expense = 1
|
||||
budget.action_if_annual_exceeded_on_cumulative_expense = (
|
||||
args.action_if_annual_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
budget.action_if_accumulated_monthly_exceeded_on_cumulative_expense = (
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
budget.insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
if args.submit_budget:
|
||||
budget.submit()
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
return budget
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-12 23:31:03.841996",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"amount",
|
||||
"percent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "percent",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percent"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 13:18:28.398198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Distribution",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BudgetDistribution(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
end_date: DF.Date | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
percent: DF.Percent
|
||||
start_date: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -462,8 +462,9 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Cost Center",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Cost Center 2",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Write Off Cost Center",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
}
|
||||
]
|
||||
18
erpnext/accounts/doctype/cost_center/test_records.toml
Normal file
18
erpnext/accounts/doctype/cost_center/test_records.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Cost Center"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Cost Center 2"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Write Off Cost Center"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
@@ -154,7 +154,3 @@ class CostCenterAllocation(Document):
|
||||
).format(d.cost_center),
|
||||
InvalidChildCostCenter,
|
||||
)
|
||||
|
||||
def clear_cache(self):
|
||||
frappe.clear_cache(doctype="Cost Center")
|
||||
return super().clear_cache()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
@@ -191,31 +190,6 @@ class TestCostCenterAllocation(IntegrationTestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestCouponCode(IntegrationTestCase):
|
||||
item_code="_Test Tesla Car",
|
||||
rate=5000,
|
||||
qty=1,
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertEqual(so.items[0].rate, 5000)
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev") {
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user