Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
16f13c75de 📝 Add docstrings to fix-mt940-statement-number-parsing
Docstrings generation was requested by @srujan00123.

* https://github.com/frappe/erpnext/pull/49682#issuecomment-3322321445

The following files were modified:

* `erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py`
* `erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py`
2025-09-23 03:58:23 +00:00
1681 changed files with 438635 additions and 609279 deletions

View File

@@ -45,9 +45,3 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
# `frappe.flags.in_test` => `frappe.in_test` # `frappe.flags.in_test` => `frappe.in_test`
7a482a69985c952de0e8193c9d4e086aee65ee6d 7a482a69985c952de0e8193c9d4e086aee65ee6d
# these commits actually changed something valuable
# but they have a lot of whitespace changes that make blame noisy
# PR: https://github.com/frappe/erpnext/pull/49816
3ffd50c772735877b330d010c1058f623da8721d
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6

View File

@@ -1,4 +1,4 @@
### Introduction (for first timers) ### Introduction (first timers)
Thank you for your interest in raising an Issue with ERPNext. An Issue could mean a bug report or a request for a missing feature. By raising a bug report, you are contributing to the development of ERPNext and this is the first step of participating in the community. Bug reports are very helpful for developers as they quickly fix the issue before other users start facing it. Thank you for your interest in raising an Issue with ERPNext. An Issue could mean a bug report or a request for a missing feature. By raising a bug report, you are contributing to the development of ERPNext and this is the first step of participating in the community. Bug reports are very helpful for developers as they quickly fix the issue before other users start facing it.
@@ -6,31 +6,31 @@ 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. 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 ### Reply and Closing Policy
If your issue is not clear or does not meet the guidelines, then it will be closed. If it is closed, please supply the requested information and re-open it. If your issue is not clear or does not meet the guidelines, then it will be closed. If it is closed, please supply the information asked and re-open it.
### General Issue Guidelines ### General Issue Guidelines
1. **Search existing Issues:** Before raising an Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. 1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created.
2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. 1. **Report each issue separately:** Don't club multiple, unreleated issues in one note.
3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. 1. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs.
### Bug Report Guidelines ### Bug Report Guidelines
1. **Steps to Reproduce:** The bug report must have a list of steps needed to reproduce a bug. If we cannot reproduce it, then we cannot solve it. 1. **Steps to Reproduce:** The bug report must have a list of steps needed to reproduce a bug. If we cannot reproduce it, then we cannot solve it.
2. **Version Number:** Please add the version number in your report. Often a bug is fixed in the latest version. 1. **Version Number:** Please add the version number in your report. Often a bug is fixed in the latest version
3. **Clear Title:** Add a clear subject to your bug report like "Unable to submit Purchase Order without Basic Rate" instead of just "Cannot Submit". 1. **Clear Title:** Add a clear subject to your bug report like "Unable to submit Purchase Order without Basic Rate" instead of just "Cannot Submit"
4. **Screenshots:** Screenshots are a great way of communicating issues. Try adding annotations or using LICEcap to take a screencast in `.gif` format. 1. **Screenshots:** Screenshots are a great way of communicating issues. Try adding annotations or using LiceCAP to take a screencast in `gif`.
### Feature Request Guidelines ### Feature Request Guidelines
1. **Clarity:** Clearly specify how you want the feature to behave. Don't just say "I would like multiple PDF formats", instead say "Ability to add multiple print formats for customers with different languages". 1. **Clarity:** Clearly specify how do you want the feature to behave. Don't just say "I would like multiple PDF formats", say that "Ability to add multiple print formats for customers with different languages".
2. **Solution:** Try to identify what the feature should look like. 1. **Solution:** Try and identify how the feature should look like.
3. **Mockups:** Mockups are a great way to explain your requirement. 1. **Mockups:** Mockups are a great way to explain your requirement.
### What if my issue is closed ### What if my Issue is closed
Don't worry, take the feedback, supply the correct information and re-open it! Don't worry, take the feedback, supply the correct information and re-open it!

View File

@@ -9,7 +9,7 @@ body:
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 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 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. - 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 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. maintainers to reproduce, the faster it'll be fixed.
@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using. description: Share exact version number of Frappe and ERPNext you are using.
placeholder: | placeholder: |
Frappe version - Frappe version -
ERPNext version - ERPNext Verion -
validations: validations:
required: true required: true

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Community Forum - name: Community Forum
url: https://discuss.frappe.io/c/erpnext/6 url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help. about: For general QnA, discussions and community help.

View File

@@ -1,6 +1,6 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea or enhancement for ERPNext about: Suggest an idea to improve ERPNext
title: '' title: ''
labels: feature-request labels: feature-request
assignees: '' assignees: ''
@@ -11,27 +11,23 @@ assignees: ''
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 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 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 2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion. the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. 3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
Please keep in mind that we get many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature. Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
If you're in urgent need of a feature, please try the following channels to get paid developments done quickly: 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 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 3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
--> -->
## Before Submitting
- [ ] I searched existing issues and confirmed this is not a duplicate
- [ ] This is a feature request, not a bug or support question
- [ ] For support: https://discuss.frappe.io/c/erpnext/6
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. As a [role], I have to [painful task] because [missing feature]. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the solution you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
@@ -39,17 +35,5 @@ A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or features you've considered.
**Impact**
<!-- Check one: -->
- [ ] Blocks critical workflow — no viable workaround
- [ ] Significant friction — workaround exists but is painful
- [ ] Nice to have — minor improvement
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
**Environment**
- ERPNext Version: <!-- Find this in Help > About, e.g. v15.12.0 -->
- Frappe Version: <!-- Find this in Help > About, e.g. v15.10.0 -->
- Deployment: <!-- Frappe Cloud / Self-hosted / ERPNext Cloud -->

View File

@@ -14,7 +14,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout Actions - name: Checkout Actions
uses: actions/checkout@v6 uses: actions/checkout@v2
with: with:
repository: "frappe/backport" repository: "frappe/backport"
path: ./actions path: ./actions

View File

@@ -13,12 +13,12 @@ jobs:
steps: steps:
- name: 'Setup Environment' - name: 'Setup Environment'
uses: actions/setup-python@v6 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: '3.10'
- name: 'Clone repo' - name: 'Clone repo'
uses: actions/checkout@v6 uses: actions/checkout@v2
- name: Validate Docs - name: Validate Docs
env: env:

View File

@@ -15,25 +15,20 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
branch: ["develop", "version-16-hotfix"] branch: ["develop"]
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: ${{ matrix.branch }} ref: ${{ matrix.branch }}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: "3.14" python-version: "3.12"
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
- name: Run script to update POT file - name: Run script to update POT file
run: | run: |

View File

@@ -19,7 +19,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
version: ["15", "16"] version: ["14", "15"]
steps: steps:
- uses: octokit/request-action@v2.x - uses: octokit/request-action@v2.x

View File

@@ -12,12 +12,12 @@ jobs:
name: linters name: linters
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- name: Set up Python 3.14 - name: Set up Python 3.10
uses: actions/setup-python@v6 uses: actions/setup-python@v4
with: with:
python-version: '3.14' python-version: '3.10'
cache: pip cache: pip
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
@@ -27,12 +27,12 @@ jobs:
name: semgrep name: semgrep
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- name: Set up Python 3.14 - name: Set up Python 3.10
uses: actions/setup-python@v6 uses: actions/setup-python@v4
with: with:
python-version: '3.14' python-version: '3.10'
cache: pip cache: pip
- name: Download Semgrep rules - name: Download Semgrep rules
@@ -43,6 +43,3 @@ jobs:
- name: Run Semgrep rules - name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
- name: Semgrep for Test Correctness
run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml

View File

@@ -29,7 +29,7 @@ jobs:
services: services:
mysql: mysql:
image: mariadb:11.8 image: mariadb:10.6
env: env:
MARIADB_ROOT_PASSWORD: 'root' MARIADB_ROOT_PASSWORD: 'root'
ports: ports:
@@ -38,7 +38,7 @@ jobs:
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Check for valid Python & Merge Conflicts - name: Check for valid Python & Merge Conflicts
run: | run: |
@@ -49,17 +49,14 @@ jobs:
fi fi
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: | python-version: '3.11'
3.11
3.13
3.14
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 18
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts
@@ -88,7 +85,7 @@ jobs:
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-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 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
@@ -113,8 +110,8 @@ jobs:
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://frappe.io/files/erpnext-v14.sql.gz wget https://erpnext.com/files/v13-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
@@ -135,15 +132,15 @@ jobs:
# Resetup env and install apps # Resetup env and install apps
pgrep honcho | xargs kill pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env rm -rf ~/frappe-bench/env
bench -v setup env --python python$2 bench -v setup env
bench pip install -e ./apps/erpnext bench pip install -e ./apps/erpnext
bench start &>> ~/frappe-bench/bench_start.log & bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate bench --site test_site migrate
} }
update_to_version 15 3.13 update_to_version 14
update_to_version 16 3.14 update_to_version 15
echo "Updating to latest version" echo "Updating to latest version"
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"

View File

@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Entire Repository - name: Checkout Entire Repository
uses: actions/checkout@v6 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v2
with: with:
node-version: 20 node-version: 20
- name: Setup dependencies - name: Setup dependencies

View File

@@ -4,8 +4,8 @@ on:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: server-individual-tests-lightmode-develop group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true cancel-in-progress: false
permissions: permissions:
contents: read contents: read
@@ -17,11 +17,11 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }} matrix: ${{ steps.set-matrix.outputs.matrix }}
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- id: set-matrix - id: set-matrix
run: | run: |
# Use grep and find to get the list of test files # Use grep and find to get the list of test files
matrix=$(find . -path '*/test_*.py' | xargs grep -l 'def test_' | sort | awk '{ matrix=$(find . -path '*/doctype/*/test_*.py' | xargs grep -l 'def test_' | awk '{
# Remove ./ prefix, file extension, and replace / with . # Remove ./ prefix, file extension, and replace / with .
gsub(/^\.\//, "", $0) gsub(/^\.\//, "", $0)
gsub(/\.py$/, "", $0) gsub(/\.py$/, "", $0)
@@ -58,7 +58,6 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{fromJson(needs.discover.outputs.matrix)}} matrix: ${{fromJson(needs.discover.outputs.matrix)}}
max-parallel: 14
name: Test name: Test
@@ -73,17 +72,17 @@ jobs:
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: '3.14' python-version: '3.12'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 18
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts
@@ -112,7 +111,7 @@ jobs:
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-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 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
@@ -131,13 +130,4 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }} FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests - name: Run Tests
run: | run: 'cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --module ${{ matrix.test }}'
site_name=$(echo "${{matrix.test}}" | sed -e 's/.*\.\(test_.*$\)/\1/')
echo "$site_name"
mkdir ~/frappe-bench/sites/$site_name
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/$site_name/site_config.json
cd ~/frappe-bench/
bench --site $site_name reinstall --yes
bench --site $site_name set-config allow_tests true
bench --site $site_name run-tests --module ${{ matrix.test }} --lightmode

View File

@@ -15,11 +15,11 @@ jobs:
name: Check Commit Titles name: Check Commit Titles
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
with: with:
fetch-depth: 200 fetch-depth: 200
- uses: actions/setup-node@v6 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
check-latest: true check-latest: true

View File

@@ -7,7 +7,6 @@ on:
paths: paths:
- "**.js" - "**.js"
- "**.css" - "**.css"
- "**.svg"
- "**.md" - "**.md"
- "**.html" - "**.html"
- 'crowdin.yml' - 'crowdin.yml'

View File

@@ -7,7 +7,6 @@ on:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.css' - '**.css'
- '**.svg'
- '**.md' - '**.md'
- '**.html' - '**.html'
- 'crowdin.yml' - 'crowdin.yml'
@@ -41,7 +40,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
env: env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production" NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }} WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
@@ -57,7 +55,6 @@ jobs:
mysql: mysql:
image: mariadb:10.6 image: mariadb:10.6
env: env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root' MARIADB_ROOT_PASSWORD: 'root'
ports: ports:
- 3306:3306 - 3306:3306
@@ -65,12 +62,12 @@ jobs:
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: '3.14' python-version: '3.12'
- name: Check for valid Python & Merge Conflicts - name: Check for valid Python & Merge Conflicts
run: | run: |
@@ -81,9 +78,9 @@ jobs:
fi fi
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 18
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts
@@ -112,7 +109,7 @@ jobs:
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-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 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
@@ -131,9 +128,10 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }} FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
- name: Run Tests - name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --lightmode --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: env:
TYPE: server TYPE: server
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
- name: Show bench output - name: Show bench output
@@ -142,6 +140,7 @@ jobs:
- name: Upload coverage data - name: Upload coverage data
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with: with:
name: coverage-${{ matrix.container }} name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml path: /home/runner/frappe-bench/sites/coverage.xml
@@ -150,9 +149,10 @@ jobs:
name: Coverage Wrap Up name: Coverage Wrap Up
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' }}
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4

View File

@@ -47,12 +47,12 @@ jobs:
steps: steps:
- name: Clone - name: Clone
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: '3.14' python-version: '3.12'
- name: Check for valid Python & Merge Conflicts - name: Check for valid Python & Merge Conflicts
run: | run: |
@@ -63,9 +63,9 @@ jobs:
fi fi
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 18
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts
@@ -94,7 +94,7 @@ jobs:
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-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 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache

View File

@@ -50,15 +50,6 @@ pull_request_rules:
- version-15-hotfix - version-15-hotfix
assignees: assignees:
- "{{ author }}" - "{{ author }}"
- name: backport to version-16-hotfix
conditions:
- label="backport version-16-hotfix"
actions:
backport:
branches:
- version-16-hotfix
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review - name: Automatic merge on CI success and review
conditions: conditions:
- status-success=linters - status-success=linters

View File

@@ -7,17 +7,17 @@ erpnext/accounts/ @ruthra-kumar
erpnext/assets/ @khushi8112 erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar erpnext/selling @ruthra-kumar
erpnext/support/ @ruthra-kumar
erpnext/buying/ @rohitwaghchaure @mihir-kandoi erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/maintenance/ @rohitwaghchaure @mihir-kandoi erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
erpnext/quality_management/ @rohitwaghchaure @mihir-kandoi erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure @mihir-kandoi erpnext/stock/ @rohitwaghchaure @mihir-kandoi
erpnext/subcontracting/ @mihir-kandoi erpnext/subcontracting @mihir-kandoi
erpnext/projects/ @nishkagosalia
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/patches/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi erpnext/patches/ @ruthra-kumar
.github/ @ruthra-kumar @mihir-kandoi .github/ @ruthra-kumar
pyproject.toml @ruthra-kumar pyproject.toml @ruthra-kumar

View File

@@ -1,12 +1,11 @@
<div align="center"> <div align="center">
<a href="https://frappe.io/erpnext"> <a href="https://frappe.io/erpnext">
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80px"/> <img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
</a> </a>
<h2>ERPNext</h2> <h2>ERPNext</h2>
<div align="center"> <p align="center">
<p>Powerful, Intuitive and Open-Source ERP</p> <p>Powerful, Intuitive and Open-Source ERP</p>
</div> </p>
[![Learn on Frappe School](https://img.shields.io/badge/Frappe%20School-Learn%20ERPNext-blue?style=flat-square)](https://frappe.school)<br><br> [![Learn on Frappe School](https://img.shields.io/badge/Frappe%20School-Learn%20ERPNext-blue?style=flat-square)](https://frappe.school)<br><br>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml) [![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
@@ -15,7 +14,7 @@
</div> </div>
<div align="center"> <div align="center">
<img src="./erpnext/public/images/v16/hero_image.png" alt="ERPNext Hero Image"/> <img src="./erpnext/public/images/v16/hero_image.png"/>
</div> </div>
<div align="center"> <div align="center">
@@ -28,19 +27,19 @@
## ERPNext ## ERPNext
100% Open-Source ERP System to help you run your business. 100% Open-Source ERP system to help you run your business.
### Motivation ### Motivation
Running a business is a complex task - handling invoices, tracking stock, managing personnel, and other daily operations. In a market where software is sold separately to manage each of these tasks, ERPNext does all of the above and more, for free. Running a business is a complex task - handling invoices, tracking stock, managing personnel and even more ad-hoc activities. In a market where software is sold separately to manage each of these tasks, ERPNext does all of the above and more, for free.
### Key Features ### Key Features
- **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports. - **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports.
- **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment. - **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment.
- **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more! - **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more!
- **Asset Management**: From purchase to disposal, IT infrastructure to equipment. Covers every branch of your organization, all in one centralized system. - **Asset Management**: From purchase to perishment, IT infrastructure to equipment. Cover every branch of your organization, all in one centralized system.
- **Projects**: Deliver both internal and external projects on time, budget and profitability. Track tasks, timesheets, and issues by project. - **Projects**: Delivery both internal and external Projects on time, budget and Profitability. Track tasks, timesheets, and issues by project.
<details open> <details open>
@@ -53,7 +52,7 @@ Running a business is a complex task - handling invoices, tracking stock, managi
### Under the Hood ### Under the Hood
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and JavaScript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API. - [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and Javascript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API.
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework. - [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework.
@@ -61,12 +60,12 @@ Running a business is a complex task - handling invoices, tracking stock, managi
### Managed Hosting ### Managed Hosting
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly, and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications reliably and securely. You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
It handles installation, setup, upgrades, monitoring, maintenance, and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments. It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
<div> <div>
<a href="https://erpnext-demo.frappe.cloud/app/home" target="_blank" rel="noopener noreferrer"> <a href="https://erpnext-demo.frappe.cloud/app/home" target="_blank">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png"> <source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" /> <img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
@@ -75,48 +74,25 @@ It handles installation, setup, upgrades, monitoring, maintenance, and support o
</div> </div>
### Self-Hosted ### Self-Hosted
#### Docker #### Docker
See [Frappe Docker Documentation](https://github.com/frappe/frappe_docker) for full documentation & FAQ on Docker setup Prerequisites: docker, docker-compose, git. Refer [Docker Documentation](https://docs.docker.com) for more details on Docker setup.
#### Prerequisites Run following commands:
- [Docker](https://docs.docker.com/get-docker/) ```
- [Docker Compose v2](https://docs.docker.com/compose/)
- [git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git)
> For Docker basics and best practices refer to Docker's [documentation](https://docs.docker.com)
#### Demo setup
The fastest way to try ERPNext is to play in a pre-configured sandbox, in your browser, click the button below:
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/>
</a>
### Try on your environment
> **⚠️ Disposable demo only**
>
> **This setup is intended for quick evaluation. Expect to throw the environment away.** You will not be able to install custom apps to this setup. For production deployments, custom configurations, and detailed explanations, see the full documentation.
First clone the repo:
```sh
git clone https://github.com/frappe/frappe_docker git clone https://github.com/frappe/frappe_docker
cd frappe_docker cd frappe_docker
```
Then run:
```sh
docker compose -f pwd.yml up -d docker compose -f pwd.yml up -d
``` ```
Wait for a couple of minutes for ERPNext site to be created or check the `create-site` container logs before opening browser on port `8080`. (username: `Administrator`, password: `admin`)
See [Frappe Docker](https://github.com/frappe/frappe_docker/blob/main/docs/01-getting-started/03-arm64.md) for ARM based docker setup After a couple of minutes, site should be accessible on your localhost port: 8080. Use below default login credentials to access the site.
- Username: Administrator
- Password: admin
See [Frappe Docker](https://github.com/frappe/frappe_docker?tab=readme-ov-file#to-run-on-arm64-architecture-follow-this-instructions) for ARM based docker setup.
## Development Setup ## Development Setup
@@ -124,7 +100,7 @@ See [Frappe Docker](https://github.com/frappe/frappe_docker/blob/main/docs/01-ge
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details. The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the Frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt). New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
### Local ### Local
@@ -153,20 +129,20 @@ To setup the repository locally follow the steps mentioned below:
4. Open the URL `http://erpnext.localhost:8000/app` in your browser, you should see the app running 4. Open the URL `http://erpnext.localhost:8000/app` in your browser, you should see the app running
## Learning and Community ## 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://school.frappe.io) - 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. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with the 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. 4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
## Contributing ## Contributing
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines) 1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
2. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Report Security Vulnerabilities](https://erpnext.com/security)
3. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
4. [Translations](https://crowdin.com/project/frappe) 2. [Translations](https://crowdin.com/project/frappe)
## Logo and Trademark Policy ## Logo and Trademark Policy

View File

@@ -1,7 +1,7 @@
# Security Policy # Security Policy
The ERPNext team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security). The ERPNext team and community take security issues seriously. To report a security issue, fill out the form at [https://erpnext.com/security/report](https://erpnext.com/security/report).
You can help us make ERPNext and all its users more secure by following the [Reporting guidelines](https://frappe.io/security). You can help us make ERPNext and all it's users more secure by following the [Reporting guidelines](https://erpnext.com/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process. We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.

View File

@@ -18,9 +18,8 @@ We will grant permission to use the ERPNext name and logo for projects that meet
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software. - The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business). - Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
- Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed. Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations: Use of the ERPNext name and logo is additionally allowed in the following situations:

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.user import is_website_user from frappe.utils.user import is_website_user
__version__ = "17.0.0-dev" __version__ = "16.0.0-dev"
def get_default_company(user=None): def get_default_company(user=None):

View File

@@ -1,50 +0,0 @@
{
"cards": [
{
"card": "Total Outgoing Bills"
},
{
"card": "Total Incoming Bills"
},
{
"card": "Total Incoming Payment"
},
{
"card": "Total Outgoing Payment"
}
],
"charts": [
{
"chart": "Incoming Bills (Purchase Invoice)",
"width": "Half"
},
{
"chart": "Outgoing Bills (Sales Invoice)",
"width": "Half"
},
{
"chart": "Accounts Receivable Ageing",
"width": "Half"
},
{
"chart": "Accounts Payable Ageing",
"width": "Half"
},
{
"chart": "Bank Balance",
"width": "Full"
}
],
"creation": "2026-01-26 21:25:12.793893",
"dashboard_name": "Payments",
"docstatus": 0,
"doctype": "Dashboard",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"modified": "2026-01-26 21:25:12.793893",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payments",
"owner": "Administrator"
}

View File

@@ -52,7 +52,7 @@ class ERPNextAddress(Address):
@frappe.whitelist() @frappe.whitelist()
def get_shipping_address(company: str, address: str | None = None): def get_shipping_address(company, address=None):
filters = [ filters = [
["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "link_name", "=", company],

View File

@@ -9,20 +9,18 @@
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"last_synced_on": "2026-01-02 13:01:24.037552", "last_synced_on": "2020-07-22 12:19:59.879476",
"modified": "2026-01-02 13:04:57.850305", "modified": "2020-07-22 12:21:48.780513",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Balance", "name": "Bank Balance",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"roles": [],
"show_values_over_chart": 1,
"source": "Account Balance Timeline", "source": "Account Balance Timeline",
"time_interval": "Monthly", "time_interval": "Quarterly",
"timeseries": 1, "timeseries": 0,
"timespan": "Last Year", "timespan": "Last Year",
"type": "Line", "type": "Line",
"use_report_chart": 0, "use_report_chart": 0,
"y_axis": [] "y_axis": []
} }

View File

@@ -1,7 +1,7 @@
{ {
"chart_name": "Profit and Loss", "chart_name": "Profit and Loss",
"chart_type": "Report", "chart_type": "Report",
"creation": "2025-04-01 20:38:16.986176", "creation": "2020-07-17 11:25:34.448572",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
@@ -9,7 +9,7 @@
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2025-12-19 12:37:31.673782", "modified": "2023-07-19 13:08:56.470390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Profit and Loss", "name": "Profit and Loss",
@@ -17,9 +17,8 @@
"owner": "Administrator", "owner": "Administrator",
"report_name": "Profit and Loss Statement", "report_name": "Profit and Loss Statement",
"roles": [], "roles": [],
"show_values_over_chart": 1,
"timeseries": 0, "timeseries": 0,
"type": "Line", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,
"y_axis": [] "y_axis": []
} }

View File

@@ -17,15 +17,6 @@ frappe.dashboards.chart_sources["Account Balance Timeline"] = {
fieldtype: "Link", fieldtype: "Link",
options: "Account", options: "Account",
reqd: 1, reqd: 1,
default: locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"],
get_query: () => {
return {
filters: {
account_type: "Bank",
is_group: 0,
},
};
},
}, },
], ],
}; };

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_to_date, formatdate, getdate, nowdate from frappe.utils import add_to_date, formatdate, get_link_to_form, getdate, nowdate
from frappe.utils.dashboard import cache_source from frappe.utils.dashboard import cache_source
from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
@@ -13,15 +13,15 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist() @frappe.whitelist()
@cache_source @cache_source
def get( def get(
chart_name: str | None = None, chart_name=None,
chart: str | dict | None = None, chart=None,
no_cache: bool | None = None, no_cache=None,
filters: str | dict | None = None, filters=None,
from_date: str | None = None, from_date=None,
to_date: str | None = None, to_date=None,
timespan: str | None = None, timespan=None,
time_interval: str | None = None, time_interval=None,
heatmap_year: str | None = None, heatmap_year=None,
): ):
if chart_name: if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name) chart = frappe.get_doc("Dashboard Chart", chart_name)
@@ -37,14 +37,21 @@ def get(
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
account = filters.get("account") account = filters.get("account")
company = filters.get("company") filters.get("company")
if not company and not account: if not account and chart_name:
frappe.throw(_("Company and account filters not set!")) frappe.throw(
if not company: _("Account is not set for the dashboard chart {0}").format(
frappe.throw(_("Company filter not set!")) get_link_to_form("Dashboard Chart", chart_name)
if not account: )
frappe.throw(_("Account filter not set!")) )
if not frappe.db.exists("Account", account) and chart_name:
frappe.throw(
_("Account {0} does not exists in the dashboard chart {1}").format(
account, get_link_to_form("Dashboard Chart", chart_name)
)
)
if not to_date: if not to_date:
to_date = nowdate() to_date = nowdate()

View File

@@ -46,8 +46,7 @@ def validate_service_stop_date(doc):
if ( if (
old_stop_dates old_stop_dates
and old_stop_dates.get(item.name) and old_stop_dates.get(item.name)
and item.service_stop_date and item.service_stop_date != old_stop_dates.get(item.name)
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
): ):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx)) frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
@@ -318,7 +317,7 @@ def get_already_booked_amount(doc, item):
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date") accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
def _book_deferred_revenue_or_expense( def _book_deferred_revenue_or_expense(
item, item,
@@ -449,12 +448,14 @@ def process_deferred_accounting(posting_date=None):
for company in companies: for company in companies:
for record_type in ("Income", "Expense"): for record_type in ("Income", "Expense"):
doc = frappe.get_doc( doc = frappe.get_doc(
doctype="Process Deferred Accounting", dict(
company=company.name, doctype="Process Deferred Accounting",
posting_date=posting_date, company=company.name,
start_date=start_date, posting_date=posting_date,
end_date=end_date, start_date=start_date,
type=record_type, end_date=end_date,
type=record_type,
)
) )
doc.insert() doc.insert()
@@ -523,8 +524,7 @@ def make_gl_entries(
if gl_entries: if gl_entries:
try: try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True) make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
if not frappe.in_test: frappe.db.commit()
frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.in_test: if frappe.in_test:
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}") doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
@@ -606,8 +606,7 @@ def book_revenue_via_journal_entry(
if submit: if submit:
journal_entry.submit() journal_entry.submit()
if not frappe.in_test: frappe.db.commit()
frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}") doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")

View File

@@ -5,7 +5,8 @@ frappe.ui.form.on("Account", {
setup: function (frm) { setup: function (frm) {
frm.add_fetch("parent_account", "report_type", "report_type"); frm.add_fetch("parent_account", "report_type", "report_type");
frm.add_fetch("parent_account", "root_type", "root_type"); frm.add_fetch("parent_account", "root_type", "root_type");
},
onload: function (frm) {
frm.set_query("parent_account", function (doc) { frm.set_query("parent_account", function (doc) {
return { return {
filters: { filters: {
@@ -14,18 +15,7 @@ frappe.ui.form.on("Account", {
}, },
}; };
}); });
frm.set_query("account_category", function () {
if (!frm.doc.root_type) return;
return {
filters: {
root_type: ["in", [frm.doc.root_type, ""]],
},
};
});
}, },
refresh: function (frm) { refresh: function (frm) {
frm.toggle_display("account_name", frm.is_new()); frm.toggle_display("account_name", frm.is_new());
@@ -68,20 +58,12 @@ frappe.ui.form.on("Account", {
} }
} }
}, },
account_type: function (frm) { account_type: function (frm) {
if (frm.doc.is_group == 0) { if (frm.doc.is_group == 0) {
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax"); frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
frm.toggle_display("warehouse", frm.doc.account_type == "Stock"); frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
} }
}, },
root_type: function (frm) {
if (frm.doc.account_category) {
frm.set_value("account_category", "");
}
},
add_toolbar_buttons: function (frm) { add_toolbar_buttons: function (frm) {
frm.add_custom_button( frm.add_custom_button(
__("Chart of Accounts"), __("Chart of Accounts"),

View File

@@ -21,7 +21,6 @@
"account_currency", "account_currency",
"column_break1", "column_break1",
"parent_account", "parent_account",
"account_category",
"account_type", "account_type",
"tax_rate", "tax_rate",
"freeze_account", "freeze_account",
@@ -190,20 +189,13 @@
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable" "label": "Disable"
},
{
"description": "Used with Financial Report Template",
"fieldname": "account_category",
"fieldtype": "Link",
"label": "Account Category",
"options": "Account Category"
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2026-04-14 18:14:42.202065", "modified": "2025-01-22 10:40:35.766017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account", "name": "Account",
@@ -256,17 +248,8 @@
"role": "Accounts Manager", "role": "Accounts Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
} }
], ],
"row_format": "Dynamic",
"search_fields": "account_number", "search_fields": "account_number",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"show_preview_popup": 1, "show_preview_popup": 1,
@@ -274,4 +257,4 @@
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -31,7 +31,6 @@ class Account(NestedSet):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
account_category: DF.Link | None
account_currency: DF.Link | None account_currency: DF.Link | None
account_name: DF.Data account_name: DF.Data
account_number: DF.Data | None account_number: DF.Data | None
@@ -93,10 +92,8 @@ class Account(NestedSet):
super().on_update() super().on_update()
def onload(self): def onload(self):
role_allowed_for_frozen_entries = frappe.db.get_value( frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
"Company", self.company, "role_allowed_for_frozen_entries" if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
)
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
self.set_onload("can_freeze_account", True) self.set_onload("can_freeze_account", True)
def autoname(self): def autoname(self):
@@ -111,7 +108,6 @@ class Account(NestedSet):
self.validate_parent_child_account_type() self.validate_parent_child_account_type()
self.validate_root_details() self.validate_root_details()
self.validate_account_number() self.validate_account_number()
self.validate_disabled()
self.validate_group_or_ledger() self.validate_group_or_ledger()
self.set_root_and_report_type() self.set_root_and_report_type()
self.validate_mandatory() self.validate_mandatory()
@@ -256,14 +252,6 @@ class Account(NestedSet):
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_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): def validate_group_or_ledger(self):
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group): if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
@@ -274,41 +262,18 @@ class Account(NestedSet):
elif cint(self.is_group): elif cint(self.is_group):
if self.account_type and not self.flags.exclude_account_type_check: if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected.")) throw(_("Cannot covert to Group because Account Type is selected."))
self.validate_default_accounts_in_company()
elif self.check_if_child_exists(): elif self.check_if_child_exists():
throw(_("Account with child nodes cannot be set as ledger")) 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): def validate_frozen_accounts_modifier(self):
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account: if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
return return
role_allowed_for_frozen_entries = frappe.get_cached_value( frozen_accounts_modifier = frappe.get_cached_value(
"Company", self.company, "role_allowed_for_frozen_entries" "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")) throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self): def validate_balance_must_be_debit_or_credit(self):
@@ -471,7 +436,7 @@ class Account(NestedSet):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql(
"""select name from tabAccount """select name from tabAccount
where is_group = 1 and docstatus != 2 and company = {} where is_group = 1 and docstatus != 2 and company = {}
@@ -515,9 +480,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist() @frappe.whitelist()
def update_account_number( def update_account_number(name, account_name, account_number=None, from_descendant=False):
name: str, account_name: str, account_number: str | None = None, from_descendant: bool = False
):
_ensure_idle_system() _ensure_idle_system()
account = frappe.get_cached_doc("Account", name) account = frappe.get_cached_doc("Account", name)
if not account: if not account:
@@ -579,7 +542,7 @@ def update_account_number(
@frappe.whitelist() @frappe.whitelist()
def merge_account(old: str, new: str): def merge_account(old, new):
_ensure_idle_system() _ensure_idle_system()
# Validate properties before merging # Validate properties before merging
new_account = frappe.get_cached_doc("Account", new) new_account = frappe.get_cached_doc("Account", new)
@@ -616,7 +579,7 @@ def merge_account(old: str, new: str):
@frappe.whitelist() @frappe.whitelist()
def get_root_company(company: str): def get_root_company(company):
# return the topmost company in the hierarchy # return the topmost company in the hierarchy
ancestors = get_ancestors_of("Company", company, "lft asc") ancestors = get_ancestors_of("Company", company, "lft asc")
return [ancestors[0]] if ancestors else [] return [ancestors[0]] if ancestors else []
@@ -662,27 +625,3 @@ def _ensure_idle_system():
).format(pretty_date(last_gl_update)), ).format(pretty_date(last_gl_update)),
title=_("System In Use"), 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",
}

View File

@@ -52,55 +52,59 @@ frappe.treeview_settings["Account"] = {
], ],
root_label: "Accounts", root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children", get_tree_nodes: "erpnext.accounts.utils.get_children",
on_node_render: function (node, deep) { on_get_node: function (nodes, deep = false) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance !== undefined) {
node.parent && node.parent.find(".balance-area").remove();
$(
'<span class="balance-area pull-right">' +
(account.account_currency != account.company_currency
? format(account.balance_in_account_currency, account.account_currency) +
" / "
: "") +
format(account.balance, account.company_currency) +
" " +
dr_or_cr +
"</span>"
).insertBefore(node.$ul);
}
}
};
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return; if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
if (!cur_tree.account_balance_data) {
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { let accounts = [];
if (value) { if (deep) {
frappe.call({ // in case of `get_all_nodes`
method: "erpnext.accounts.utils.get_account_balances_coa", accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
args: {
company: cur_tree.args.company,
include_default_fb_balances: true,
},
callback: function (r) {
if (!r.message || r.message.length === 0) return;
cur_tree.account_balance_data = r.message || [];
render_balances();
},
});
}
});
} else { } else {
render_balances(); accounts = nodes;
} }
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if (value) {
const get_balances = frappe.call({
method: "erpnext.accounts.utils.get_account_balances",
args: {
accounts: accounts,
company: cur_tree.args.company,
},
});
get_balances.then((r) => {
if (!r.message || r.message.length == 0) return;
for (let account of r.message) {
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance !== undefined) {
node.parent && node.parent.find(".balance-area").remove();
$(
'<span class="balance-area pull-right">' +
(account.balance_in_account_currency
? format(
account.balance_in_account_currency,
account.account_currency
) + " / "
: "") +
format(account.balance, account.company_currency) +
" " +
dr_or_cr +
"</span>"
).insertBefore(node.$ul);
}
}
});
}
});
}, },
add_tree_node: "erpnext.accounts.utils.add_ac", add_tree_node: "erpnext.accounts.utils.add_ac",
menu_items: [ menu_items: [
@@ -156,14 +160,6 @@ frappe.treeview_settings["Account"] = {
.options, .options,
description: __("Optional. This setting will be used to filter in various transactions."), 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", fieldtype: "Float",
fieldname: "tax_rate", fieldname: "tax_rate",

View File

@@ -23,7 +23,15 @@ def create_charts(
if root_account: if root_account:
root_type = child.get("root_type") root_type = child.get("root_type")
if account_name not in 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_number = cstr(child.get("account_number")).strip()
account_name, account_name_in_db = add_suffix_if_duplicate( account_name, account_name_in_db = add_suffix_if_duplicate(
account_name, account_number, accounts account_name, account_number, accounts
@@ -47,7 +55,6 @@ def create_charts(
"report_type": report_type, "report_type": report_type,
"account_number": account_number, "account_number": account_number,
"account_type": child.get("account_type"), "account_type": child.get("account_type"),
"account_category": child.get("account_category"),
"account_currency": child.get("account_currency") "account_currency": child.get("account_currency")
if custom_chart if custom_chart
else frappe.get_cached_value("Company", company, "default_currency"), else frappe.get_cached_value("Company", company, "default_currency"),
@@ -90,7 +97,20 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child): def identify_is_group(child):
if child.get("is_group"): if child.get("is_group"):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len(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 is_group = 1
else: else:
is_group = 0 is_group = 0
@@ -99,7 +119,7 @@ def identify_is_group(child):
@frappe.whitelist() @frappe.whitelist()
def get_chart(chart_template: str | None, existing_company: str | None = None): def get_chart(chart_template, existing_company=None):
chart = {} chart = {}
if existing_company: if existing_company:
return get_account_tree_from_existing_company(existing_company) return get_account_tree_from_existing_company(existing_company)
@@ -132,7 +152,7 @@ def get_chart(chart_template: str | None, existing_company: str | None = None):
@frappe.whitelist() @frappe.whitelist()
def get_charts_for_country(country: str, with_standard: bool = False): def get_charts_for_country(country, with_standard=False):
charts = [] charts = []
def _get_chart_name(content): def _get_chart_name(content):
@@ -225,7 +245,7 @@ def build_account_tree(tree, parent, all_accounts):
@frappe.whitelist() @frappe.whitelist()
def validate_bank_account(coa: str, bank_account: str): def validate_bank_account(coa, bank_account):
accounts = [] accounts = []
chart = get_chart(coa) chart = get_chart(coa)
@@ -233,7 +253,13 @@ def validate_bank_account(coa: str, bank_account: str):
def _get_account_names(account_master): def _get_account_names(account_master):
for account_name, child in account_master.items(): for account_name, child in account_master.items():
if account_name not in get_chart_metadata_fields(): if account_name not in [
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
accounts.append(account_name) accounts.append(account_name)
_get_account_names(child) _get_account_names(child)
@@ -244,9 +270,7 @@ def validate_bank_account(coa: str, bank_account: str):
@frappe.whitelist() @frappe.whitelist()
def build_tree_from_json( def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
chart_template: str, chart_data: dict | None = None, from_coa_importer: bool = False
):
"""get chart template from its folder and parse the json to be rendered as tree""" """get chart template from its folder and parse the json to be rendered as tree"""
chart = chart_data or get_chart(chart_template) chart = chart_data or get_chart(chart_template)
@@ -260,7 +284,15 @@ def build_tree_from_json(
"""recursively called to form a parent-child based list of dict from chart template""" """recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in children.items(): for account_name, child in children.items():
account = {} account = {}
if account_name in get_chart_metadata_fields(): if account_name in [
"account_name",
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
continue continue
if from_coa_importer: if from_coa_importer:
@@ -278,16 +310,3 @@ def build_tree_from_json(
_import_accounts(chart, None) _import_accounts(chart, None)
return accounts return accounts
def get_chart_metadata_fields():
return [
"account_name",
"account_number",
"account_type",
"account_category",
"root_type",
"is_group",
"tax_rate",
"account_currency",
]

View File

@@ -33,17 +33,6 @@
}, },
"account_number": "1151.000" "account_number": "1151.000"
}, },
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1152.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.002",
"account_type": "Tax"
},
"account_number": "1152.000"
},
"account_number": "1150.000" "account_number": "1150.000"
}, },
"Kas": { "Kas": {
@@ -108,6 +97,17 @@
}, },
"account_number": "1130.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" "account_number": "1100.000"
}, },

View File

@@ -6,83 +6,64 @@
"Current Assets": { "Current Assets": {
"Accounts Receivable": { "Accounts Receivable": {
"Debtors": { "Debtors": {
"account_type": "Receivable", "account_type": "Receivable"
"account_category": "Trade Receivables"
} }
}, },
"Bank Accounts": { "Bank Accounts": {
"account_type": "Bank", "account_type": "Bank",
"is_group": 1, "is_group": 1
"account_category": "Cash and Cash Equivalents"
}, },
"Cash In Hand": { "Cash In Hand": {
"Cash": { "Cash": {
"account_type": "Cash", "account_type": "Cash"
"account_category": "Cash and Cash Equivalents"
}, },
"account_type": "Cash", "account_type": "Cash"
"account_category": "Cash and Cash Equivalents"
}, },
"Loans and Advances (Assets)": { "Loans and Advances (Assets)": {
"is_group": 1, "is_group": 1
"account_category": "Other Receivables"
}, },
"Securities and Deposits": { "Securities and Deposits": {
"Earnest Money": { "Earnest Money": {}
"account_category": "Other Current Assets"
}
}, },
"Stock Assets": { "Stock Assets": {
"Stock In Hand": { "Stock In Hand": {
"account_type": "Stock", "account_type": "Stock"
"account_category": "Stock Assets"
}, },
"account_type": "Stock", "account_type": "Stock"
"account_category": "Stock Assets"
}, },
"Tax Assets": { "Tax Assets": {
"is_group": 1, "is_group": 1
"account_category": "Other Current Assets"
} }
}, },
"Fixed Assets": { "Fixed Assets": {
"Capital Equipment": { "Capital Equipment": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Electronic Equipment": { "Electronic Equipment": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Furniture and Fixtures": { "Furniture and Fixtures": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Office Equipment": { "Office Equipment": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Plants and Machineries": { "Plants and Machineries": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Buildings": { "Buildings": {
"account_type": "Fixed Asset", "account_type": "Fixed Asset"
"account_category": "Tangible Assets"
}, },
"Accumulated Depreciations": { "Accumulated Depreciations": {
"account_type": "Accumulated Depreciation", "account_type": "Accumulated Depreciation"
"account_category": "Tangible Assets"
} }
}, },
"Investments": { "Investments": {
"is_group": 1, "is_group": 1
"account_category": "Long-term Investments"
}, },
"Temporary Accounts": { "Temporary Accounts": {
"Temporary Opening": { "Temporary Opening": {
"account_type": "Temporary", "account_type": "Temporary"
"account_category": "Other Non-current Assets"
} }
}, },
"root_type": "Asset" "root_type": "Asset"
@@ -91,103 +72,55 @@
"Direct Expenses": { "Direct Expenses": {
"Stock Expenses": { "Stock Expenses": {
"Cost of Goods Sold": { "Cost of Goods Sold": {
"account_type": "Cost of Goods Sold", "account_type": "Cost of Goods Sold"
"account_category": "Cost of Goods Sold"
}, },
"Expenses Included In Valuation": { "Expenses Included In Valuation": {
"account_type": "Expenses Included In Valuation", "account_type": "Expenses Included In Valuation"
"account_category": "Other Direct Costs"
}, },
"Stock Adjustment": { "Stock Adjustment": {
"account_type": "Stock Adjustment", "account_type": "Stock Adjustment"
"account_category": "Other Direct Costs"
} }
} }
}, },
"Indirect Expenses": { "Indirect Expenses": {
"Administrative Expenses": { "Administrative Expenses": {},
"account_category": "Operating Expenses" "Commission on Sales": {},
},
"Commission on Sales": {
"account_category": "Operating Expenses"
},
"Depreciation": { "Depreciation": {
"account_type": "Depreciation", "account_type": "Depreciation"
"account_category": "Operating Expenses"
},
"Entertainment Expenses": {
"account_category": "Operating Expenses"
}, },
"Entertainment Expenses": {},
"Freight and Forwarding Charges": { "Freight and Forwarding Charges": {
"account_type": "Chargeable", "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"
}, },
"Legal Expenses": {},
"Marketing Expenses": {},
"Miscellaneous Expenses": {},
"Office Maintenance Expenses": {},
"Office Rent": {},
"Postal Expenses": {},
"Print and Stationery": {},
"Rounded Off": { "Rounded Off": {
"account_type": "Round Off", "account_type": "Round Off"
"account_category": "Operating Expenses"
}, },
"Salary": { "Salary": {},
"account_category": "Operating Expenses" "Sales Expenses": {},
}, "Telephone Expenses": {},
"Sales Expenses": { "Travel Expenses": {},
"account_category": "Operating Expenses" "Utility Expenses": {},
}, "Write Off": {},
"Telephone Expenses": { "Exchange Gain/Loss": {},
"account_category": "Operating Expenses" "Gain/Loss on Asset Disposal": {},
}, "Impairment": {}
"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"
},
"Gain/Loss on Asset Disposal": {
"account_category": "Other Operating Income"
},
"Impairment": {
"account_category": "Operating Expenses"
}
}, },
"root_type": "Expense" "root_type": "Expense"
}, },
"Income": { "Income": {
"Direct Income": { "Direct Income": {
"Sales": { "Sales": {
"account_type": "Income Account", "account_type": "Income Account"
"account_category": "Revenue from Operations"
}, },
"Service": { "Service": {
"account_type": "Income Account", "account_type": "Income Account"
"account_category": "Revenue from Operations"
}, },
"account_type": "Income Account" "account_type": "Income Account"
}, },
@@ -199,51 +132,31 @@
}, },
"Source of Funds (Liabilities)": { "Source of Funds (Liabilities)": {
"Capital Account": { "Capital Account": {
"Reserves and Surplus": { "Reserves and Surplus": {},
"account_category": "Reserves and Surplus" "Shareholders Funds": {},
}, "Revaluation Surplus": {}
"Shareholders Funds": {
"account_category": "Share Capital"
},
"Revaluation Surplus": {
"account_category": "Reserves and Surplus"
}
}, },
"Current Liabilities": { "Current Liabilities": {
"Accounts Payable": { "Accounts Payable": {
"Creditors": { "Creditors": {
"account_type": "Payable", "account_type": "Payable"
"account_category": "Trade Payables"
}, },
"Payroll Payable": { "Payroll Payable": {}
"account_category": "Other Payables"
}
}, },
"Stock Liabilities": { "Stock Liabilities": {
"Stock Received But Not Billed": { "Stock Received But Not Billed": {
"account_type": "Stock Received But Not Billed", "account_type": "Stock Received But Not Billed"
"account_category": "Trade Payables"
} }
}, },
"Duties and Taxes": { "Duties and Taxes": {
"TDS": { "TDS": {
"account_type": "Tax", "account_type": "Tax"
"account_category": "Current Tax Liabilities" }
},
"account_type": "Tax",
"is_group": 1,
"account_category": "Current Tax Liabilities"
}, },
"Loans (Liabilities)": { "Loans (Liabilities)": {
"Secured Loans": { "Secured Loans": {},
"account_category": "Long-term Borrowings" "Unsecured Loans": {},
}, "Bank Overdraft Account": {}
"Unsecured Loans": {
"account_category": "Long-term Borrowings"
},
"Bank Overdraft Account": {
"account_category": "Short-term Borrowings"
}
} }
}, },
"root_type": "Liability" "root_type": "Liability"

View File

@@ -9,192 +9,103 @@ def get():
return { return {
_("Application of Funds (Assets)"): { _("Application of Funds (Assets)"): {
_("Current Assets"): { _("Current Assets"): {
_("Accounts Receivable"): { _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
_("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"} _("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
}, _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
_("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",
},
_("Loans and Advances (Assets)"): { _("Loans and Advances (Assets)"): {
_("Employee Advances"): { _("Employee Advances"): {"account_type": "Payable"},
"account_type": "Payable",
"account_category": "Other Receivables",
},
}, },
_("Securities and Deposits"): { _("Securities and Deposits"): {_("Earnest Money"): {}},
_("Earnest Money"): {"account_category": "Other Current Assets"}
},
_("Prepaid Expenses"): {"account_category": "Other Current Assets"},
_("Short-term Investments"): {"account_category": "Short-term Investments"},
_("Stock Assets"): { _("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"}, _("Stock In Hand"): {"account_type": "Stock"},
"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"): { _("Fixed Assets"): {
_("Capital Equipment"): { _("Capital Equipment"): {"account_type": "Fixed Asset"},
"account_type": "Fixed Asset", _("Electronic Equipment"): {"account_type": "Fixed Asset"},
"account_category": "Tangible Assets", _("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
}, _("Office Equipment"): {"account_type": "Fixed Asset"},
_("Electronic Equipment"): { _("Plants and Machineries"): {"account_type": "Fixed Asset"},
"account_type": "Fixed Asset", _("Buildings"): {"account_type": "Fixed Asset"},
"account_category": "Tangible Assets", _("Software"): {"account_type": "Fixed Asset"},
}, _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
_("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",
},
_("CWIP Account"): { _("CWIP Account"): {
"account_type": "Capital Work in Progress", "account_type": "Capital Work in Progress",
"account_category": "Tangible Assets",
}, },
}, },
_("Investments"): {"is_group": 1, "account_category": "Long-term Investments"}, _("Investments"): {"is_group": 1},
_("Temporary Accounts"): { _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
_("Temporary Opening"): {
"account_type": "Temporary",
"account_category": "Other Non-current Assets",
}
},
"root_type": "Asset", "root_type": "Asset",
}, },
_("Expenses"): { _("Expenses"): {
_("Direct Expenses"): { _("Direct Expenses"): {
_("Stock Expenses"): { _("Stock Expenses"): {
_("Cost of Goods Sold"): { _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
"account_type": "Cost of Goods Sold",
"account_category": "Cost of Goods Sold",
},
_("Expenses Included In Asset Valuation"): { _("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation", "account_type": "Expenses Included In Asset Valuation"
"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",
}, },
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
}, },
}, },
_("Indirect Expenses"): { _("Indirect Expenses"): {
_("Administrative Expenses"): {"account_category": "Operating Expenses"}, _("Administrative Expenses"): {},
_("Commission on Sales"): {"account_category": "Operating Expenses"}, _("Commission on Sales"): {},
_("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"}, _("Depreciation"): {"account_type": "Depreciation"},
_("Entertainment Expenses"): {"account_category": "Operating Expenses"}, _("Entertainment Expenses"): {},
_("Freight and Forwarding Charges"): { _("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
"account_type": "Chargeable", _("Legal Expenses"): {},
"account_category": "Operating Expenses", _("Marketing Expenses"): {"account_type": "Chargeable"},
}, _("Miscellaneous Expenses"): {"account_type": "Chargeable"},
_("Legal Expenses"): {"account_category": "Operating Expenses"}, _("Office Maintenance Expenses"): {},
_("Marketing Expenses"): { _("Office Rent"): {},
"account_type": "Chargeable", _("Postal Expenses"): {},
"account_category": "Operating Expenses", _("Print and Stationery"): {},
}, _("Round Off"): {"account_type": "Round Off"},
_("Miscellaneous Expenses"): { _("Salary"): {},
"account_type": "Chargeable", _("Sales Expenses"): {},
"account_category": "Operating Expenses", _("Telephone Expenses"): {},
}, _("Travel Expenses"): {},
_("Office Maintenance Expenses"): {"account_category": "Operating Expenses"}, _("Utility Expenses"): {},
_("Office Rent"): {"account_category": "Operating Expenses"}, _("Write Off"): {},
_("Postal Expenses"): {"account_category": "Operating Expenses"}, _("Exchange Gain/Loss"): {},
_("Print and Stationery"): {"account_category": "Operating Expenses"}, _("Gain/Loss on Asset Disposal"): {},
_("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"}, _("Impairment"): {},
_("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"},
}, },
"root_type": "Expense", "root_type": "Expense",
}, },
_("Income"): { _("Income"): {
_("Direct Income"): { _("Direct Income"): {_("Sales"): {}, _("Service"): {}},
_("Sales"): {"account_category": "Revenue from Operations"}, _("Indirect Income"): {"is_group": 1},
_("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,
},
"root_type": "Income", "root_type": "Income",
}, },
_("Source of Funds (Liabilities)"): { _("Source of Funds (Liabilities)"): {
_("Current Liabilities"): { _("Current Liabilities"): {
_("Accounts Payable"): { _("Accounts Payable"): {
_("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"}, _("Creditors"): {"account_type": "Payable"},
_("Payroll Payable"): {"account_category": "Other Payables"}, _("Payroll Payable"): {},
}, },
_("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
_("Customer Advances"): {"account_category": "Other Current Liabilities"},
_("Stock Liabilities"): { _("Stock Liabilities"): {
_("Stock Received But Not Billed"): { _("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
"account_type": "Stock Received But Not Billed", _("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
"account_category": "Trade Payables",
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed",
"account_category": "Trade Payables",
},
}, },
_("Duties and Taxes"): { _("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
"account_type": "Tax",
"is_group": 1,
"account_category": "Current Tax Liabilities",
},
_("Short-term Provisions"): {"account_category": "Short-term Provisions"},
_("Loans (Liabilities)"): { _("Loans (Liabilities)"): {
_("Secured Loans"): {"account_category": "Long-term Borrowings"}, _("Secured Loans"): {},
_("Unsecured Loans"): {"account_category": "Long-term Borrowings"}, _("Unsecured Loans"): {},
_("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"}, _("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", "root_type": "Liability",
}, },
_("Equity"): { _("Equity"): {
_("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"}, _("Capital Stock"): {"account_type": "Equity"},
_("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"}, _("Dividends Paid"): {"account_type": "Equity"},
_("Opening Balance Equity"): { _("Opening Balance Equity"): {"account_type": "Equity"},
"account_type": "Equity", _("Retained Earnings"): {"account_type": "Equity"},
"account_category": "Reserves and Surplus", _("Revaluation Surplus"): {"account_type": "Equity"},
},
_("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
_("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
"root_type": "Equity", "root_type": "Equity",
}, },
} }

View File

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

View File

@@ -1,7 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import ( from erpnext.accounts.doctype.account.account import (
@@ -10,10 +12,11 @@ from erpnext.accounts.doctype.account.account import (
update_account_number, update_account_number,
) )
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Company"]
class TestAccount(ERPNextTestSuite): class TestAccount(IntegrationTestCase):
def test_rename_account(self): def test_rename_account(self):
if not frappe.db.exists("Account", "1210 - Debtors - _TC"): if not frappe.db.exists("Account", "1210 - Debtors - _TC"):
acc = frappe.new_doc("Account") acc = frappe.new_doc("Account")
@@ -320,6 +323,72 @@ class TestAccount(ERPNextTestSuite):
self.assertEqual(balance, 0) self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.tests.utils import make_test_objects
accounts = [
# [account_name, parent_account, is_group]
["_Test Bank", "Bank Accounts", 0, "Bank", None],
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account CST", "Direct Expenses", 0, "Tax", None],
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
for company, abbr in [
["_Test Company", "_TC"],
["_Test Company 1", "_TC1"],
["_Test Company with perpetual inventory", "TCP1"],
]:
test_objects = make_test_objects(
"Account",
[
{
"doctype": "Account",
"account_name": account_name,
"parent_account": parent_account + " - " + abbr,
"company": company,
"is_group": is_group,
"account_type": account_type,
"account_currency": currency,
}
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects
def get_inventory_account(company, warehouse=None): def get_inventory_account(company, warehouse=None):
account = None account = None
if warehouse: if warehouse:
@@ -346,13 +415,15 @@ def create_account(**kwargs):
return account.name return account.name
else: else:
account = frappe.get_doc( account = frappe.get_doc(
doctype="Account", dict(
is_group=kwargs.get("is_group", 0), doctype="Account",
account_name=kwargs.get("account_name"), is_group=kwargs.get("is_group", 0),
account_type=kwargs.get("account_type"), account_name=kwargs.get("account_name"),
parent_account=kwargs.get("parent_account"), account_type=kwargs.get("account_type"),
company=kwargs.get("company"), parent_account=kwargs.get("parent_account"),
account_currency=kwargs.get("account_currency"), company=kwargs.get("company"),
account_currency=kwargs.get("account_currency"),
)
) )
account.save() account.save()

View File

@@ -0,0 +1,6 @@
[
{
"doctype": "Account",
"name": "_Test Account 1"
}
]

View File

@@ -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) {
// },
// });

View File

@@ -1,91 +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",
"root_type",
"column_break_qluu",
"description"
],
"fields": [
{
"fieldname": "account_category_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Category Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "column_break_qluu",
"fieldtype": "Column Break"
},
{
"fieldname": "root_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Account",
"link_fieldname": "account_category"
}
],
"modified": "2026-03-05 06:49:34.430723",
"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, root_type",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,95 +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
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
# 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)

View File

@@ -1,4 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Account Closing Balance", { // frappe.ui.form.on("Account Closing Balance", {
refresh(frm) { // refresh(frm) {
frm.page.btn_secondary.hide();
}, // },
}); // });

View File

@@ -5,7 +5,6 @@
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
"is_submittable": 1,
"field_order": [ "field_order": [
"closing_date", "closing_date",
"account", "account",

View File

@@ -2,9 +2,8 @@
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests import IntegrationTestCase
from erpnext.tests.utils import ERPNextTestSuite
class TestAccountClosingBalance(ERPNextTestSuite): class TestAccountClosingBalance(IntegrationTestCase):
pass pass

View File

@@ -82,7 +82,7 @@ class AccountingDimension(Document):
else: else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company))) frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def on_update(self): def after_insert(self):
if frappe.in_test: if frappe.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
@@ -103,6 +103,10 @@ class AccountingDimension(Document):
if not self.fieldname: if not self.fieldname:
self.fieldname = scrub(self.label) self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None): def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist: if not doclist:
@@ -206,7 +210,7 @@ def delete_accounting_dimension(doc):
@frappe.whitelist() @frappe.whitelist()
def disable_dimension(doc: str): def disable_dimension(doc):
if frappe.in_test: if frappe.in_test:
toggle_disabling(doc=doc) toggle_disabling(doc=doc)
else: else:
@@ -237,26 +241,34 @@ def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes") return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True, filters=None):
accounting_dimensions = frappe.get_all( if not filters:
"Accounting Dimension", filters = {"disabled": 0}
fields=["label", "fieldname", "disabled", "document_type"],
filters={"disabled": 0}, if frappe.flags.accounting_dimensions is None:
) frappe.flags.accounting_dimensions = frappe.get_all(
"Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters=filters,
)
if as_list: if as_list:
return [d.fieldname for d in accounting_dimensions] return [d.fieldname for d in frappe.flags.accounting_dimensions]
else: else:
return accounting_dimensions return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts(): def get_checks_for_pl_and_bs_accounts():
return frappe.db.sql( if frappe.flags.accounting_dimensions_details is None:
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs # nosemgrep
frappe.flags.accounting_dimensions_details = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent AND p.disabled = 0""", WHERE p.name = c.parent AND p.disabled = 0""",
as_dict=1, as_dict=1,
) )
return frappe.flags.accounting_dimensions_details
def get_dimension_with_children(doctype, dimensions): def get_dimension_with_children(doctype, dimensions):
@@ -274,7 +286,7 @@ def get_dimension_with_children(doctype, dimensions):
@frappe.whitelist() @frappe.whitelist()
def get_dimensions(with_cost_center_and_project: str | bool = False): def get_dimensions(with_cost_center_and_project=False):
c = frappe.qb.DocType("Accounting Dimension Detail") c = frappe.qb.DocType("Accounting Dimension Detail")
p = frappe.qb.DocType("Accounting Dimension") p = frappe.qb.DocType("Accounting Dimension")
dimension_filters = ( dimension_filters = (
@@ -297,8 +309,8 @@ def get_dimensions(with_cost_center_and_project: str | bool = False):
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend( dimension_filters.extend(
[ [
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}), {"fieldname": "cost_center", "document_type": "Cost Center"},
frappe._dict({"fieldname": "project", "document_type": "Project"}), {"fieldname": "project", "document_type": "Project"},
] ]
) )

View File

@@ -1,14 +1,20 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Cost Center", "Location", "Warehouse", "Department"]
class TestAccountingDimension(ERPNextTestSuite): class TestAccountingDimension(IntegrationTestCase):
def setUp(self):
create_dimension()
def test_dimension_against_sales_invoice(self): def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
@@ -73,3 +79,68 @@ class TestAccountingDimension(ERPNextTestSuite):
si.save() si.save()
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
def tearDown(self):
disable_dimension()
frappe.flags.accounting_dimensions_details = None
frappe.flags.dimension_filter_map = None
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
dimension = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
}
)
dimension.append(
"dimension_defaults",
{
"company": "_Test Company",
"reference_document": "Department",
"default_dimension": "_Test Department - _TC",
},
)
dimension.insert()
dimension.save()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Location",
}
)
dimension1.append(
"dimension_defaults",
{
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
},
)
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1
dimension1.save()
dimension2 = frappe.get_doc("Accounting Dimension", "Location")
dimension2.disabled = 1
dimension2.save()

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _, scrub
from frappe.model.document import Document from frappe.model.document import Document
@@ -69,34 +69,37 @@ class AccountingDimensionFilter(Document):
def get_dimension_filter_map(): def get_dimension_filter_map():
filters = frappe.db.sql( if not frappe.flags.get("dimension_filter_map"):
""" filters = frappe.db.sql(
SELECT """
a.applicable_on_account, d.dimension_value, p.accounting_dimension, SELECT
p.allow_or_restrict, p.fieldname, a.is_mandatory a.applicable_on_account, d.dimension_value, p.accounting_dimension,
FROM p.allow_or_restrict, p.fieldname, a.is_mandatory
`tabApplicable On Account` a, FROM
`tabAccounting Dimension Filter` p `tabApplicable On Account` a,
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name `tabAccounting Dimension Filter` p
WHERE LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
p.name = a.parent WHERE
AND p.disabled = 0 p.name = a.parent
""", AND p.disabled = 0
as_dict=1, """,
) as_dict=1,
dimension_filter_map = {}
for f in filters:
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
) )
return dimension_filter_map
dimension_filter_map = {}
for f in filters:
build_map(
dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
frappe.flags.dimension_filter_map = dimension_filter_map
return frappe.flags.dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):

View File

@@ -1,15 +1,23 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Location", "Cost Center", "Department"]
class TestAccountingDimensionFilter(ERPNextTestSuite): class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self): def setUp(self):
create_dimension()
create_accounting_dimension_filter() create_accounting_dimension_filter()
self.invoice_list = [] self.invoice_list = []
@@ -36,6 +44,17 @@ class TestAccountingDimensionFilter(ERPNextTestSuite):
self.assertRaises(MandatoryAccountDimensionError, si.submit) self.assertRaises(MandatoryAccountDimensionError, si.submit)
self.invoice_list.append(si) self.invoice_list.append(si)
def tearDown(self):
disable_dimension_filter()
disable_dimension()
frappe.flags.accounting_dimensions_details = None
frappe.flags.dimension_filter_map = None
for si in self.invoice_list:
si.load_from_db()
if si.docstatus == 1:
si.cancel()
def create_accounting_dimension_filter(): def create_accounting_dimension_filter():
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}): if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}):

View File

@@ -11,8 +11,6 @@
"end_date", "end_date",
"column_break_4", "column_break_4",
"company", "company",
"disabled",
"exempted_role",
"section_break_7", "section_break_7",
"closed_documents" "closed_documents"
], ],
@@ -20,6 +18,7 @@
{ {
"fieldname": "period_name", "fieldname": "period_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Period Name", "label": "Period Name",
"reqd": 1, "reqd": 1,
"unique": 1 "unique": 1
@@ -50,13 +49,6 @@
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
}, },
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled"
},
{ {
"fieldname": "section_break_7", "fieldname": "section_break_7",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -67,22 +59,13 @@
"label": "Closed Documents", "label": "Closed Documents",
"options": "Closed Document", "options": "Closed Document",
"reqd": 1 "reqd": 1
},
{
"description": "Role allowed to bypass period restrictions.",
"fieldname": "exempted_role",
"fieldtype": "Link",
"label": "Exempted Role",
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
"options": "Role"
} }
], ],
"links": [], "links": [],
"modified": "2026-03-09 17:15:33.577217", "modified": "2024-03-27 13:05:57.388109",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Period", "name": "Accounting Period",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -122,9 +105,8 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -28,9 +28,7 @@ class AccountingPeriod(Document):
closed_documents: DF.Table[ClosedDocument] closed_documents: DF.Table[ClosedDocument]
company: DF.Link company: DF.Link
disabled: DF.Check
end_date: DF.Date end_date: DF.Date
exempted_role: DF.Link | None
period_name: DF.Data period_name: DF.Data
start_date: DF.Date start_date: DF.Date
# end: auto-generated types # end: auto-generated types
@@ -97,7 +95,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance": if doc.doctype == "Bank Clearance":
return return
elif doc.doctype == "Asset": elif doc.doctype == "Asset":
if doc.asset_type == "Existing Asset": if doc.is_existing_asset:
return return
else: else:
date = doc.available_for_use_date date = doc.available_for_use_date
@@ -114,11 +112,10 @@ def validate_accounting_period_on_doc_save(doc, method=None):
accounting_period = ( accounting_period = (
frappe.qb.from_(ap) frappe.qb.from_(ap)
.from_(cd) .from_(cd)
.select(ap.name, ap.exempted_role) .select(ap.name)
.where( .where(
(ap.name == cd.parent) (ap.name == cd.parent)
& (ap.company == doc.company) & (ap.company == doc.company)
& (ap.disabled == 0)
& (cd.closed == 1) & (cd.closed == 1)
& (cd.document_type == doc.doctype) & (cd.document_type == doc.doctype)
& (date >= ap.start_date) & (date >= ap.start_date)
@@ -127,11 +124,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
).run(as_dict=1) ).run(as_dict=1)
if accounting_period: if accounting_period:
if (
accounting_period[0].get("exempted_role")
and accounting_period[0].get("exempted_role") in frappe.get_roles()
):
return
frappe.throw( frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format( _("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"]) doc.doctype, frappe.bold(accounting_period[0]["name"])

View File

@@ -1,7 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, nowdate from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import ( from erpnext.accounts.doctype.accounting_period.accounting_period import (
@@ -9,10 +11,11 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import (
OverlapError, OverlapError,
) )
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item"]
class TestAccountingPeriod(ERPNextTestSuite): class TestAccountingPeriod(IntegrationTestCase):
def test_overlap(self): def test_overlap(self):
ap1 = create_accounting_period( ap1 = create_accounting_period(
start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC" start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
@@ -34,58 +37,9 @@ class TestAccountingPeriod(ERPNextTestSuite):
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
self.assertRaises(ClosedAccountingPeriod, doc.save) self.assertRaises(ClosedAccountingPeriod, doc.save)
def test_accounting_period_exempted_role(self): def tearDown(self):
# Create Accounting Period with exempted role for d in frappe.get_all("Accounting Period"):
ap = create_accounting_period( frappe.delete_doc("Accounting Period", d.name)
period_name="Test Accounting Period Exempted",
exempted_role="Accounts Manager",
start_date="2025-12-01",
end_date="2025-12-31",
)
ap.save()
# Create users
users = frappe.get_all("User", filters={"email": ["like", "test%"]}, limit=1)
user = None
if users[0].name:
user = frappe.get_doc("User", users[0].name)
else:
user = frappe.get_doc(
{
"doctype": "User",
"email": "test1@example.com",
"first_name": "Test1",
}
)
user.insert()
user.roles = []
user.append("roles", {"role": "Accounts User"})
# ---- Non-exempted user should FAIL ----
user.save(ignore_permissions=True)
frappe.clear_cache(user=user.name)
frappe.set_user(user.name)
posting_date = "2025-12-11"
doc = create_sales_invoice(
do_not_save=1,
posting_date=posting_date,
)
with self.assertRaises(frappe.ValidationError):
doc.submit()
# ---- Exempted role should PASS ----
user.append("roles", {"role": "Accounts Manager"})
user.save(ignore_permissions=True)
frappe.clear_cache(user=user.name)
doc = create_sales_invoice(do_not_save=1, posting_date=posting_date)
doc.submit() # Should not raise
self.assertEqual(doc.docstatus, 1)
def create_accounting_period(**args): def create_accounting_period(**args):
@@ -97,6 +51,5 @@ def create_accounting_period(**args):
accounting_period.company = args.company or "_Test Company" accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1" accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1}) accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
accounting_period.exempted_role = args.exempted_role or ""
return accounting_period return accounting_period

View File

@@ -2,15 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Accounts Settings", { frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) { refresh: function (frm) {},
frm.set_query("document_type", "repost_allowed_types", function (doc, cdt, cdn) {
return {
filters: {
name: ["in", frappe.boot.sysdefaults.repost_allowed_doctypes],
},
};
});
},
enable_immutable_ledger: function (frm) { enable_immutable_ledger: function (frm) {
if (!frm.doc.enable_immutable_ledger) { if (!frm.doc.enable_immutable_ledger) {
return; return;

View File

@@ -16,15 +16,10 @@
"invoicing_features_section", "invoicing_features_section",
"check_supplier_invoice_uniqueness", "check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"enable_subscription",
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date", "confirm_before_resetting_posting_date",
"analytics_section",
"enable_accounting_dimensions",
"column_break_vtnr",
"enable_discounts_and_margin",
"journals_section", "journals_section",
"merge_similar_account_heads", "merge_similar_account_heads",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@@ -56,19 +51,12 @@
"allow_pegged_currencies_exchange_rates", "allow_pegged_currencies_exchange_rates",
"column_break_yuug", "column_break_yuug",
"stale_days", "stale_days",
"payments_tab",
"section_break_jpd0", "section_break_jpd0",
"auto_reconcile_payments", "auto_reconcile_payments",
"auto_reconciliation_job_trigger", "auto_reconciliation_job_trigger",
"reconciliation_queue_size", "reconciliation_queue_size",
"column_break_resa", "column_break_resa",
"exchange_gain_loss_posting_date", "exchange_gain_loss_posting_date",
"repost_section",
"repost_allowed_types",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
"fetch_payment_schedule_in_payment_request",
"invoicing_settings_tab", "invoicing_settings_tab",
"accounts_transactions_settings_section", "accounts_transactions_settings_section",
"over_billing_allowance", "over_billing_allowance",
@@ -76,17 +64,26 @@
"role_allowed_to_over_bill", "role_allowed_to_over_bill",
"credit_controller", "credit_controller",
"make_payment_via_journal_entry", "make_payment_via_journal_entry",
"pos_tab",
"pos_setting_section",
"post_change_gl_entries",
"column_break_xrnd",
"assets_tab", "assets_tab",
"asset_settings_section", "asset_settings_section",
"calculate_depr_using_total_days", "calculate_depr_using_total_days",
"column_break_gjcc", "column_break_gjcc",
"book_asset_depreciation_entry_automatically", "book_asset_depreciation_entry_automatically",
"role_to_notify_on_depreciation_failure",
"closing_settings_tab", "closing_settings_tab",
"period_closing_settings_section", "period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance", "ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"column_break_25", "column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
"show_balance_in_coa",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching",
"reports_tab", "reports_tab",
"remarks_section", "remarks_section",
"general_ledger_remarks_length", "general_ledger_remarks_length",
@@ -94,23 +91,31 @@
"receivable_payable_remarks_length", "receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section", "accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method", "receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi", "column_break_ntmi",
"drop_ar_procedures", "drop_ar_procedures",
"legacy_section", "legacy_section",
"ignore_is_opening_check_for_reporting", "ignore_is_opening_check_for_reporting",
"tab_break_dpet", "payment_request_settings",
"chart_of_accounts_section",
"show_balance_in_coa",
"banking_section",
"enable_party_matching",
"enable_fuzzy_matching",
"payment_request_section",
"create_pr_in_draft_status", "create_pr_in_draft_status",
"budget_section", "budget_settings",
"use_legacy_budget_controller" "use_new_budget_controller"
], ],
"fields": [ "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", "default": "Billing Address",
"description": "Address used to determine Tax Category in transactions", "description": "Address used to determine Tax Category in transactions",
@@ -209,7 +214,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is", "description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms", "fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order/Quotation" "label": "Automatically Fetch Payment Terms from Order"
}, },
{ {
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ", "description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -291,9 +296,16 @@
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "1",
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries",
"fieldtype": "Check",
"label": "Create Ledger Entries for Change Amount"
},
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\" rel=\"noopener noreferrer\">Common Party</a>", "description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_common_party_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Common Party Accounting"
@@ -331,6 +343,11 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Accounts Closing" "label": "Accounts Closing"
}, },
{
"fieldname": "pos_setting_section",
"fieldtype": "Section Break",
"label": "POS Setting"
},
{ {
"fieldname": "invoice_and_billing_tab", "fieldname": "invoice_and_billing_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
@@ -345,6 +362,11 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "pos_tab",
"fieldtype": "Tab Break",
"label": "POS"
},
{ {
"default": "0", "default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
@@ -355,7 +377,7 @@
{ {
"fieldname": "tab_break_dpet", "fieldname": "tab_break_dpet",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Others" "label": "Chart Of Accounts"
}, },
{ {
"default": "1", "default": "1",
@@ -399,6 +421,11 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Taxes as Table in Print" "label": "Show Taxes as Table in Print"
}, },
{
"fieldname": "banking_tab",
"fieldtype": "Tab Break",
"label": "Banking"
},
{ {
"default": "0", "default": "0",
"description": "Auto match and set the Party in Bank Transactions", "description": "Auto match and set the Party in Bank Transactions",
@@ -474,9 +501,14 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Calculate daily depreciation using total days in depreciation period" "label": "Calculate daily depreciation using total days in depreciation period"
}, },
{
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
"fieldname": "payment_request_settings",
"fieldtype": "Tab Break",
"label": "Payment Request"
},
{ {
"default": "1", "default": "1",
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
"fieldname": "create_pr_in_draft_status", "fieldname": "create_pr_in_draft_status",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Create in Draft Status" "label": "Create in Draft Status"
@@ -518,6 +550,10 @@
"label": "Posting Date Inheritance for Exchange Gain / Loss", "label": "Posting Date Inheritance for Exchange Gain / Loss",
"options": "Invoice\nPayment\nReconciliation Date" "options": "Invoice\nPayment\nReconciliation Date"
}, },
{
"fieldname": "column_break_xrnd",
"fieldtype": "Column Break"
},
{ {
"default": "Buffered Cursor", "default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method", "fieldname": "receivable_payable_fetch_method",
@@ -557,6 +593,17 @@
"label": "Role Allowed to Override Stop Action", "label": "Role Allowed to Override Stop Action",
"options": "Role" "options": "Role"
}, },
{
"fieldname": "budget_settings",
"fieldtype": "Tab Break",
"label": "Budget"
},
{
"default": "1",
"fieldname": "use_new_budget_controller",
"fieldtype": "Check",
"label": "Use New Budget Controller"
},
{ {
"default": "1", "default": "1",
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions", "description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
@@ -604,118 +651,6 @@
"fieldname": "fetch_valuation_rate_for_internal_transaction", "fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction" "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"
},
{
"default": "30, 60, 90, 120",
"fieldname": "default_ageing_range",
"fieldtype": "Data",
"label": "Default Ageing Range"
},
{
"fieldname": "chart_of_accounts_section",
"fieldtype": "Section Break",
"label": "Chart Of Accounts"
},
{
"fieldname": "banking_section",
"fieldtype": "Section Break",
"label": "Banking"
},
{
"fieldname": "payment_request_section",
"fieldtype": "Section Break",
"label": "Payment Request"
},
{
"fieldname": "budget_section",
"fieldtype": "Section Break",
"label": "Budget"
},
{
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytical Accounting"
},
{
"fieldname": "column_break_vtnr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Apply discounts and margins on products",
"fieldname": "enable_discounts_and_margin",
"fieldtype": "Check",
"label": "Enable Discounts and Margin"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "payment_options_section",
"fieldtype": "Section Break",
"label": "Payment Options"
},
{
"default": "0",
"fieldname": "enable_loyalty_point_program",
"fieldtype": "Check",
"label": "Enable Loyalty Point Program"
},
{
"fieldname": "column_break_ctam",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable cost center, projects and other custom accounting dimensions",
"fieldname": "enable_accounting_dimensions",
"fieldtype": "Check",
"label": "Enable Accounting Dimensions"
},
{
"default": "1",
"description": "Enable Subscription tracking in invoice",
"fieldname": "enable_subscription",
"fieldtype": "Check",
"label": "Enable Subscription"
},
{
"default": "1",
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
},
{
"fieldname": "repost_section",
"fieldtype": "Section Break",
"label": "Repost"
},
{
"fieldname": "repost_allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -724,7 +659,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-04-13 15:30:28.729627", "modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -10,32 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import sync_auto_reconcile_config from erpnext.accounts.utils import sync_auto_reconcile_config
from erpnext.stock.utils import check_pending_reposting
SELLING_DOCTYPES = [
"Sales Invoice",
"Sales Order",
"Delivery Note",
"Quotation",
"Sales Invoice Item",
"Sales Order Item",
"Delivery Note Item",
"Quotation Item",
"POS Invoice",
"POS Invoice Item",
]
BUYING_DOCTYPES = [
"Purchase Invoice",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice Item",
"Purchase Order Item",
"Purchase Receipt Item",
]
class AccountsSettings(Document): class AccountsSettings(Document):
@@ -47,8 +23,7 @@ class AccountsSettings(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -67,20 +42,15 @@ class AccountsSettings(Document):
confirm_before_resetting_posting_date: DF.Check confirm_before_resetting_posting_date: DF.Check
create_pr_in_draft_status: DF.Check create_pr_in_draft_status: DF.Check
credit_controller: DF.Link | None credit_controller: DF.Link | None
default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
enable_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
enable_fuzzy_matching: DF.Check enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_payment_schedule_in_payment_request: DF.Check
fetch_valuation_rate_for_internal_transaction: DF.Check fetch_valuation_rate_for_internal_transaction: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check ignore_is_opening_check_for_reporting: DF.Check
@@ -89,12 +59,11 @@ class AccountsSettings(Document):
make_payment_via_journal_entry: DF.Check make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency 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_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_remarks_length: DF.Int receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_over_bill: DF.Link | None 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 role_to_override_stop_action: DF.Link | None
round_row_wise_tax: DF.Check round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check show_balance_in_coa: DF.Check
@@ -105,8 +74,7 @@ class AccountsSettings(Document):
submit_journal_entries: DF.Check submit_journal_entries: DF.Check
unlink_advance_payment_on_cancelation_of_order: DF.Check unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check unlink_payment_on_cancellation_of_invoice: DF.Check
use_legacy_budget_controller: DF.Check use_new_budget_controller: DF.Check
use_legacy_controller_for_pcv: DF.Check
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
@@ -131,27 +99,13 @@ class AccountsSettings(Document):
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print: if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
if old_doc.enable_accounting_dimensions != self.enable_accounting_dimensions: if old_doc.acc_frozen_upto != self.acc_frozen_upto:
toggle_accounting_dimension_sections(not self.enable_accounting_dimensions) self.validate_pending_reposts()
clear_cache = True
if old_doc.enable_discounts_and_margin != self.enable_discounts_and_margin:
toggle_sales_discount_section(not self.enable_discounts_and_margin)
clear_cache = True
if old_doc.enable_loyalty_point_program != self.enable_loyalty_point_program:
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if old_doc.enable_subscription != self.enable_subscription:
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if clear_cache: if clear_cache:
frappe.clear_cache() frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config() self.validate_and_sync_auto_reconcile_config()
self.update_property_for_accounting_dimension()
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -174,6 +128,10 @@ class AccountsSettings(Document):
validate_fields_for_doctype=False, 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): def validate_and_sync_auto_reconcile_config(self):
if self.has_value_changed("auto_reconciliation_job_trigger"): if self.has_value_changed("auto_reconciliation_job_trigger"):
if ( if (
@@ -198,68 +156,10 @@ class AccountsSettings(Document):
title=_("Auto Tax Settings Error"), title=_("Auto Tax Settings Error"),
) )
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.repost_allowed_types]
if not doctypes:
return
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
@frappe.whitelist() @frappe.whitelist()
def drop_ar_sql_procedures(self): def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR 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.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
for doctype in accounting_dimension_doctypes:
create_property_setter_for_hiding_field(doctype, "accounting_dimensions_section", hide)
def toggle_sales_discount_section(hide):
for doctype in SELLING_DOCTYPES + BUYING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("additional_discount_section"):
create_property_setter_for_hiding_field(doctype, "additional_discount_section", hide)
if meta.has_field("discount_and_margin"):
create_property_setter_for_hiding_field(doctype, "discount_and_margin", hide)
def toggle_loyalty_point_program_section(hide):
for doctype in SELLING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("loyalty_points_redemption"):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def toggle_subscription_sections(hide):
subscription_doctypes = frappe.get_hooks("subscription_doctypes")
for doctype in subscription_doctypes:
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
field_name,
"hidden",
hide,
"Check",
validate_fields_for_doctype=False,
)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -1,16 +1,11 @@
frappe.ui.form.on("Accounts Settings", { frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) { refresh: function (frm) {
frm.set_df_property("credit_controller", "label", "Credit Manager"); frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
},
});
frappe.ui.form.on("Company", {
refresh: function (frm) {
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
frm.set_df_property( frm.set_df_property(
"role_allowed_for_frozen_entries", "frozen_accounts_modifier",
"label", "label",
"Role Allowed to Close Books & Make Changes to Closed Periods" "Role Allowed to Close Books & Make Changes to Closed Periods"
); );
frm.set_df_property("credit_controller", "label", "Credit Manager");
}, },
}); });

View File

@@ -1,9 +1,17 @@
import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsSettings(ERPNextTestSuite): class TestAccountsSettings(IntegrationTestCase):
def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break
cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 1
cur_settings.save()
def test_stale_days(self): def test_stale_days(self):
cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 0 cur_settings.allow_stale = 0

View File

@@ -1,10 +1,8 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Advance Payment Ledger Entry", { // frappe.ui.form.on("Advance Payment Ledger Entry", {
refresh(frm) { // refresh(frm) {
frm.page.btn_secondary.hide();
frm.set_currency_labels(["amount"], frm.doc.currency); // },
frm.set_currency_labels(["base_amount"], erpnext.get_currency(frm.doc.company)); // });
},
});

View File

@@ -4,17 +4,14 @@
"creation": "2024-10-16 16:57:12.085072", "creation": "2024-10-16 16:57:12.085072",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"is_submittable": 1,
"field_order": [ "field_order": [
"company", "company",
"voucher_type", "voucher_type",
"voucher_no", "voucher_no",
"against_voucher_type", "against_voucher_type",
"against_voucher_no", "against_voucher_no",
"currency",
"exchange_rate",
"amount", "amount",
"base_amount", "currency",
"event", "event",
"delinked" "delinked"
], ],
@@ -51,7 +48,6 @@
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount", "label": "Amount",
"options": "currency",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -80,29 +76,13 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "DeLinked", "label": "DeLinked",
"read_only": 1 "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, "grid_page_length": 50,
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-11-13 12:45:03.014555", "modified": "2025-07-29 11:37:42.678556",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Advance Payment Ledger Entry", "name": "Advance Payment Ledger Entry",

View File

@@ -19,12 +19,10 @@ class AdvancePaymentLedgerEntry(Document):
against_voucher_no: DF.DynamicLink | None against_voucher_no: DF.DynamicLink | None
against_voucher_type: DF.Link | None against_voucher_type: DF.Link | None
amount: DF.Currency amount: DF.Currency
base_amount: DF.Currency
company: DF.Link | None company: DF.Link | None
currency: DF.Link | None currency: DF.Link | None
delinked: DF.Check delinked: DF.Check
event: DF.Data | None event: DF.Data | None
exchange_rate: DF.Float
voucher_no: DF.DynamicLink | None voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None voucher_type: DF.Link | None
# end: auto-generated types # end: auto-generated types
@@ -36,15 +34,3 @@ class AdvancePaymentLedgerEntry(Document):
and not frappe.flags.is_reverse_depr_entry and not frappe.flags.is_reverse_depr_entry
): ):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None) 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"],
)

View File

@@ -2,6 +2,7 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate, today from frappe.utils import nowdate, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -9,13 +10,14 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
# On ERPNextTestSuite, the doctype test records and all # On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded # link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list # Use these module variables to add/remove to/from that list
from erpnext.tests.utils import ERPNextTestSuite EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin): class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase):
""" """
Integration tests for AdvancePaymentLedgerEntry. Integration tests for AdvancePaymentLedgerEntry.
Use this class for testing interactions between multiple components. Use this class for testing interactions between multiple components.
@@ -28,6 +30,9 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
self.create_item() self.create_item()
self.clear_old_entries() self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False): def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
""" """
Helper method Helper method

View File

@@ -0,0 +1,57 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-25 10:24:39.836195",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_detail",
"account_head",
"allocated_amount"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType"
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_type"
},
{
"fieldname": "reference_detail",
"fieldtype": "Data",
"label": "Reference Detail"
},
{
"fieldname": "account_head",
"fieldtype": "Link",
"label": "Account Head",
"options": "Account"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "party_account_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:05:58.308002",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Tax",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,11 +1,11 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class BudgetDistribution(Document): class AdvanceTax(Document):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@@ -14,13 +14,14 @@ class BudgetDistribution(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
amount: DF.Currency account_head: DF.Link | None
end_date: DF.Date | None allocated_amount: DF.Currency
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
percent: DF.Percent reference_detail: DF.Data | None
start_date: DF.Date | None reference_name: DF.DynamicLink | None
reference_type: DF.Link | None
# end: auto-generated types # end: auto-generated types
pass pass

View File

@@ -14,7 +14,6 @@
"description", "description",
"included_in_paid_amount", "included_in_paid_amount",
"set_by_item_tax_template", "set_by_item_tax_template",
"is_tax_withholding_account",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
@@ -26,6 +25,7 @@
"net_amount", "net_amount",
"tax_amount", "tax_amount",
"total", "total",
"allocated_amount",
"column_break_13", "column_break_13",
"base_tax_amount", "base_tax_amount",
"base_net_amount", "base_net_amount",
@@ -97,11 +97,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{ {
"fieldname": "section_break_8", "fieldname": "section_break_8",
@@ -172,6 +172,12 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Considered In Paid Amount" "label": "Considered In Paid Amount"
}, },
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "currency"
},
{ {
"fetch_from": "account_head.account_currency", "fetch_from": "account_head.account_currency",
"fieldname": "currency", "fieldname": "currency",
@@ -207,26 +213,18 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"report_hide": 1 "report_hide": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-12-15 06:42:18.707671", "modified": "2024-11-22 19:16:22.346267",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Advance Taxes and Charges", "name": "Advance Taxes and Charges",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": []
} }

View File

@@ -17,6 +17,7 @@ class AdvanceTaxesandCharges(Document):
account_head: DF.Link account_head: DF.Link
add_deduct_tax: DF.Literal["Add", "Deduct"] add_deduct_tax: DF.Literal["Add", "Deduct"]
allocated_amount: DF.Currency
base_net_amount: DF.Currency base_net_amount: DF.Currency
base_tax_amount: DF.Currency base_tax_amount: DF.Currency
base_total: DF.Currency base_total: DF.Currency
@@ -27,12 +28,10 @@ class AdvanceTaxesandCharges(Document):
currency: DF.Link | None currency: DF.Link | None
description: DF.SmallText description: DF.SmallText
included_in_paid_amount: DF.Check included_in_paid_amount: DF.Check
is_tax_withholding_account: DF.Check
net_amount: DF.Currency net_amount: DF.Currency
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
project: DF.Link | None
rate: DF.Float rate: DF.Float
row_id: DF.Data | None row_id: DF.Data | None
set_by_item_tax_template: DF.Check set_by_item_tax_template: DF.Check

View File

@@ -3,6 +3,9 @@
frappe.provide("erpnext.integrations"); frappe.provide("erpnext.integrations");
frappe.ui.form.on("Bank", { frappe.ui.form.on("Bank", {
onload: function (frm) {
add_fields_to_mapping_table(frm);
},
refresh: function (frm) { refresh: function (frm) {
add_fields_to_mapping_table(frm); add_fields_to_mapping_table(frm);
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
@@ -34,11 +37,11 @@ let add_fields_to_mapping_table = function (frm) {
}); });
}); });
const grid = frm.fields_dict.bank_transaction_mapping?.grid; frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
"bank_transaction_field",
if (grid) { "options",
grid.update_docfield_property("bank_transaction_field", "options", options); options
} );
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -113,7 +116,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
"There was an issue connecting to Plaid's authentication server. Check browser console for more information" "There was an issue connecting to Plaid's authentication server. Check browser console for more information"
) )
); );
console.error(error); console.log(error);
} }
plaid_success(token, response) { plaid_success(token, response) {

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
from erpnext.tests.utils import ERPNextTestSuite from frappe.tests import IntegrationTestCase
class TestBank(ERPNextTestSuite): class TestBank(IntegrationTestCase):
pass pass

View File

@@ -42,4 +42,8 @@ frappe.ui.form.on("Bank Account", {
}); });
} }
}, },
is_company_account: function (frm) {
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
},
}); });

View File

@@ -52,7 +52,6 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Company Account", "label": "Company Account",
"mandatory_depends_on": "is_company_account",
"options": "Account" "options": "Account"
}, },
{ {
@@ -99,7 +98,6 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"mandatory_depends_on": "is_company_account",
"options": "Company" "options": "Company"
}, },
{ {
@@ -254,7 +252,7 @@
"link_fieldname": "default_bank_account" "link_fieldname": "default_bank_account"
} }
], ],
"modified": "2026-01-20 00:46:16.633364", "modified": "2025-08-29 12:32:01.081687",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Account", "name": "Bank Account",

View File

@@ -51,29 +51,25 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name) delete_contact_and_address("Bank Account", self.name)
def validate(self): def validate(self):
self.validate_is_company_account() self.validate_company()
self.validate_account()
self.update_default_bank_account() self.update_default_bank_account()
def validate_is_company_account(self):
if self.is_company_account:
if not self.company:
frappe.throw(_("Company is mandatory for company account"))
if not self.account:
frappe.throw(_("Company Account is mandatory"))
self.validate_account()
def validate_account(self): def validate_account(self):
if accounts := frappe.db.get_all( if self.account:
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 if accounts := frappe.db.get_all(
): "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
frappe.throw( ):
_("'{0}' account is already used by {1}. Use another account.").format( frappe.throw(
frappe.bold(self.account), _("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
) )
)
def validate_company(self):
if self.is_company_account and not self.company:
frappe.throw(_("Company is mandatory for company account"))
def update_default_bank_account(self): def update_default_bank_account(self):
if self.is_default and not self.disabled: if self.is_default and not self.disabled:
@@ -115,8 +111,7 @@ def get_default_company_bank_account(company, party_type, party):
@frappe.whitelist() @frappe.whitelist()
def get_bank_account_details(bank_account: str): def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value( return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
) )

View File

@@ -1,8 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
from erpnext.tests.utils import ERPNextTestSuite import frappe
from frappe import ValidationError
from frappe.tests import IntegrationTestCase
class TestBankAccount(ERPNextTestSuite): class TestBankAccount(IntegrationTestCase):
pass pass

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
from erpnext.tests.utils import ERPNextTestSuite from frappe.tests import IntegrationTestCase
class TestBankAccountSubtype(ERPNextTestSuite): class TestBankAccountSubtype(IntegrationTestCase):
pass pass

View File

@@ -1,9 +1,10 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe # import frappe
import unittest
from erpnext.tests.utils import ERPNextTestSuite from frappe.tests import IntegrationTestCase
class TestBankAccountType(ERPNextTestSuite): class TestBankAccountType(IntegrationTestCase):
pass pass

View File

@@ -5,10 +5,8 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import cint, flt, fmt_money, get_link_to_form, getdate
from frappe.utils import cint, flt, fmt_money, getdate
from pypika import Order from pypika import Order
import erpnext import erpnext
@@ -127,7 +125,7 @@ class BankClearance(Document):
) )
msg += "</ul>" msg += "</ul>"
msgprint(_(msg)) frappe.throw(_(msg))
return return
if not entries_to_update: if not entries_to_update:
@@ -136,44 +134,16 @@ class BankClearance(Document):
for d in entries_to_update: for d in entries_to_update:
if d.payment_document == "Sales Invoice": if d.payment_document == "Sales Invoice":
old_clearance_date = frappe.db.get_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
{ {"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"parent": d.payment_entry,
"account": self.account,
"amount": [">", 0],
},
"clearance_date", "clearance_date",
d.clearance_date,
) )
if d.clearance_date or old_clearance_date:
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"clearance_date",
d.clearance_date,
)
sales_invoice = frappe.get_lazy_doc("Sales Invoice", d.payment_entry)
sales_invoice.add_comment(
"Comment",
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
old_clearance_date, d.clearance_date
),
)
else: else:
# using db_set to trigger notification
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry) payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
old_clearance_date = payment_entry.clearance_date payment_entry.db_set("clearance_date", d.clearance_date)
if d.clearance_date or old_clearance_date:
# using db_set to trigger notification
payment_entry.db_set("clearance_date", d.clearance_date)
payment_entry.add_comment(
"Comment",
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
old_clearance_date, d.clearance_date
),
)
self.get_payment_entries() self.get_payment_entries()
msgprint(_("Clearance Date updated")) msgprint(_("Clearance Date updated"))
@@ -184,162 +154,65 @@ def get_payment_entries_for_bank_clearance(
): ):
entries = [] entries = []
journal_entry = frappe.qb.DocType("Journal Entry") condition = ""
journal_entry_account = frappe.qb.DocType("Journal Entry Account") pe_condition = ""
journal_entry_query = (
frappe.qb.from_(journal_entry_account)
.inner_join(journal_entry)
.on(journal_entry_account.parent == journal_entry.name)
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
journal_entry.name.as_("payment_entry"),
journal_entry.cheque_no.as_("cheque_number"),
journal_entry.cheque_date,
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
journal_entry.posting_date,
journal_entry_account.against_account,
journal_entry.clearance_date,
journal_entry_account.account_currency,
)
.where(
(journal_entry_account.account == account)
& (journal_entry.docstatus == 1)
& (journal_entry.posting_date >= from_date)
& (journal_entry.posting_date <= to_date)
& (journal_entry.is_opening == "No")
)
)
if not include_reconciled_entries: if not include_reconciled_entries:
journal_entry_query = journal_entry_query.where( condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00") pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
)
journal_entries = ( journal_entries = frappe.db.sql(
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name) f"""
.orderby(journal_entry.posting_date) select
.orderby(journal_entry.name, order=Order.desc) "Journal Entry" as payment_document, t1.name as payment_entry,
).run(as_dict=True) t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
pe = frappe.qb.DocType("Payment Entry") t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
company = frappe.qb.DocType("Company") from
payment_entry_query = ( `tabJournal Entry` t1, `tabJournal Entry Account` t2
frappe.qb.from_(pe) where
.join(company) t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
.on(pe.company == company.name) and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
.select( and ifnull(t1.is_opening, 'No') = 'No' {condition}
ConstantColumn("Payment Entry").as_("payment_document"), group by t2.account, t1.name
pe.name.as_("payment_entry"), order by t1.posting_date ASC, t1.name DESC
pe.reference_no.as_("cheque_number"), """,
pe.reference_date.as_("cheque_date"), {"account": account, "from": from_date, "to": to_date},
( as_dict=1,
Case()
.when(
pe.paid_from == account,
(
pe.paid_amount
+ (
Case()
.when(
(pe.payment_type == "Pay")
& (company.default_currency == pe.paid_from_account_currency),
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
),
)
.else_(0)
).as_("credit"),
(
Case()
.when(pe.paid_from == account, 0)
.else_(
pe.received_amount
+ (
Case()
.when(
company.default_currency == pe.paid_to_account_currency,
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
)
).as_("debit"),
pe.posting_date,
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
"against_account"
),
pe.clearance_date,
(
Case()
.when(pe.paid_to == account, pe.paid_to_account_currency)
.else_(pe.paid_from_account_currency)
).as_("account_currency"),
)
.where(
((pe.paid_from == account) | (pe.paid_to == account))
& (pe.docstatus == 1)
& (pe.posting_date >= from_date)
& (pe.posting_date <= to_date)
)
) )
if not include_reconciled_entries: payment_entries = frappe.db.sql(
payment_entry_query = payment_entry_query.where( f"""
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00") select
) "Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run( 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,
as_dict=True 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
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}
order by
pe.posting_date ASC, pe.name DESC
""",
{
"account": account,
"from": from_date,
"to": to_date,
},
as_dict=1,
) )
acc = frappe.qb.DocType("Account") pos_sales_invoices, pos_purchase_invoices = [], []
pi = frappe.qb.DocType("Purchase Invoice")
paid_purchase_invoices_query = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.bill_no.as_("cheque_number"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
)
if not include_reconciled_entries:
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
)
paid_purchase_invoices = (
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
).run(as_dict=True)
pos_sales_invoices = []
if include_pos_transactions: if include_pos_transactions:
si_payment = frappe.qb.DocType("Sales Invoice Payment") si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice") si = frappe.qb.DocType("Sales Invoice")
acc = frappe.qb.DocType("Account")
pos_sales_invoices_query = ( pos_sales_invoices = (
frappe.qb.from_(si_payment) frappe.qb.from_(si_payment)
.inner_join(si) .inner_join(si)
.on(si_payment.parent == si.name) .on(si_payment.parent == si.name)
@@ -362,22 +235,38 @@ def get_payment_entries_for_bank_clearance(
& (si.posting_date >= from_date) & (si.posting_date >= from_date)
& (si.posting_date <= to_date) & (si.posting_date <= to_date)
) )
) .orderby(si.posting_date)
.orderby(si.name, order=Order.desc)
).run(as_dict=True)
if not include_reconciled_entries: pi = frappe.qb.DocType("Purchase Invoice")
pos_sales_invoices_query = pos_sales_invoices_query.where(
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00") pos_purchase_invoices = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
) )
.where(
pos_sales_invoices = ( (pi.docstatus == 1)
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc) & (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
).run(as_dict=True) ).run(as_dict=True)
entries = ( entries = (
list(payment_entries) list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(paid_purchase_invoices)
) )
return entries return entries

View File

@@ -1,7 +1,9 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, getdate from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -13,12 +15,13 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(ERPNextTestSuite): class TestBankClearance(IntegrationTestCase):
def setUp(self): @classmethod
frappe.clear_cache() def setUpClass(cls):
super().setUpClass()
create_warehouse( create_warehouse(
warehouse_name="_Test Warehouse", warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"}, properties={"parent_warehouse": "All Warehouses - _TC"},

View File

@@ -30,7 +30,8 @@
"label": "Payment Entry", "label": "Payment Entry",
"oldfieldname": "voucher_id", "oldfieldname": "voucher_id",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "payment_document" "options": "payment_document",
"width": "50"
}, },
{ {
"columns": 2, "columns": 2,
@@ -68,7 +69,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 1, "columns": 2,
"fieldname": "cheque_number", "fieldname": "cheque_number",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
@@ -78,10 +79,8 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "cheque_date", "fieldname": "cheque_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1,
"label": "Cheque Date", "label": "Cheque Date",
"oldfieldname": "cheque_date", "oldfieldname": "cheque_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
@@ -97,19 +96,17 @@
"oldfieldtype": "Date" "oldfieldtype": "Date"
} }
], ],
"grid_page_length": 50,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-12-17 14:33:45.913311", "modified": "2024-03-27 13:06:37.609319",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Clearance Detail", "name": "Bank Clearance Detail",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": []
} }

View File

@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", { frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) { setup: function (frm) {
frm.set_query("bank", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("bank_account", function () { frm.set_query("bank_account", function () {
return { return {
filters: { filters: {

View File

@@ -1,8 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
from erpnext.tests.utils import ERPNextTestSuite from frappe.tests import IntegrationTestCase
class TestBankGuarantee(ERPNextTestSuite): class TestBankGuarantee(IntegrationTestCase):
pass pass

View File

@@ -1,8 +1,8 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json import json
from datetime import date
import frappe import frappe
from frappe import _ from frappe import _
@@ -47,9 +47,7 @@ class BankReconciliationTool(Document):
@frappe.whitelist() @frappe.whitelist()
def get_bank_transactions( def get_bank_transactions(bank_account, from_date=None, to_date=None):
bank_account: str, from_date: str | date | None = None, to_date: str | date | None = None
):
# returns bank transactions for a bank account # returns bank transactions for a bank account
filters = [] filters = []
filters.append(["bank_account", "=", bank_account]) filters.append(["bank_account", "=", bank_account])
@@ -59,7 +57,7 @@ def get_bank_transactions(
filters.append(["date", "<=", to_date]) filters.append(["date", "<=", to_date])
if from_date: if from_date:
filters.append(["date", ">=", from_date]) filters.append(["date", ">=", from_date])
transactions = frappe.get_list( transactions = frappe.get_all(
"Bank Transaction", "Bank Transaction",
fields=[ fields=[
"date", "date",
@@ -82,9 +80,8 @@ def get_bank_transactions(
@frappe.whitelist() @frappe.whitelist()
def get_account_balance(bank_account: str, till_date: str | date, company: str): def get_account_balance(bank_account, till_date, company):
# returns account balance till the specified date # returns account balance till the specified date
frappe.has_permission("Bank Account", "read", bank_account, throw=True)
account = frappe.db.get_value("Bank Account", bank_account, "account") account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict( filters = frappe._dict(
{ {
@@ -109,9 +106,7 @@ def get_account_balance(bank_account: str, till_date: str | date, company: str):
@frappe.whitelist() @frappe.whitelist()
def update_bank_transaction( def update_bank_transaction(bank_transaction_name, reference_number, party_type=None, party=None):
bank_transaction_name: str, reference_number: str, party_type: str | None = None, party: str | None = None
):
# updates bank transaction based on the new parameters provided by the user from Vouchers # updates bank transaction based on the new parameters provided by the user from Vouchers
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_transaction.reference_number = reference_number bank_transaction.reference_number = reference_number
@@ -140,16 +135,16 @@ def update_bank_transaction(
@frappe.whitelist() @frappe.whitelist()
def create_journal_entry_bts( def create_journal_entry_bts(
bank_transaction_name: str, bank_transaction_name,
reference_number: str | None = None, reference_number=None,
reference_date: str | None = None, reference_date=None,
posting_date: str | date | None = None, posting_date=None,
entry_type: str | None = None, entry_type=None,
second_account: str | None = None, second_account=None,
mode_of_payment: str | None = None, mode_of_payment=None,
party_type: str | None = None, party_type=None,
party: str | None = None, party=None,
allow_edit: bool | None = None, allow_edit=None,
): ):
# Create a new journal entry based on the bank transaction # Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
@@ -299,17 +294,16 @@ def create_journal_entry_bts(
@frappe.whitelist() @frappe.whitelist()
def create_payment_entry_bts( def create_payment_entry_bts(
bank_transaction_name: str, bank_transaction_name,
reference_number: str | None = None, reference_number=None,
reference_date: str | None = None, reference_date=None,
party_type: str | None = None, party_type=None,
party: str | None = None, party=None,
posting_date: str | None = None, posting_date=None,
mode_of_payment: str | None = None, mode_of_payment=None,
project: str | None = None, project=None,
cost_center: str | None = None, cost_center=None,
allow_edit: bool | None = None, allow_edit=None,
company_bank_account: str | None = None,
): ):
# Create a new payment entry based on the bank transaction # Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
@@ -351,9 +345,6 @@ def create_payment_entry_bts(
pe.project = project pe.project = project
pe.cost_center = cost_center pe.cost_center = cost_center
if company_bank_account:
pe.bank_account = company_bank_account
pe.validate() pe.validate()
if allow_edit: if allow_edit:
@@ -376,12 +367,12 @@ def create_payment_entry_bts(
@frappe.whitelist() @frappe.whitelist()
def auto_reconcile_vouchers( def auto_reconcile_vouchers(
bank_account: str, bank_account,
from_date: str | date | None = None, from_date=None,
to_date: str | date | None = None, to_date=None,
filter_by_reference_date: bool | None = None, filter_by_reference_date=None,
from_reference_date: bool | None = None, from_reference_date=None,
to_reference_date: str | None = None, to_reference_date=None,
): ):
bank_transactions = get_bank_transactions(bank_account) bank_transactions = get_bank_transactions(bank_account)
@@ -418,7 +409,7 @@ def start_auto_reconcile(
for transaction in bank_transactions: for transaction in bank_transactions:
linked_payments = get_linked_payments( linked_payments = get_linked_payments(
transaction.name, transaction.name,
["payment_entry", "journal_entry", "sales_invoice"], ["payment_entry", "journal_entry"],
from_date, from_date,
to_date, to_date,
filter_by_reference_date, filter_by_reference_date,
@@ -476,7 +467,7 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
@frappe.whitelist() @frappe.whitelist()
def reconcile_vouchers(bank_transaction_name: str, vouchers: str): def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction # updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -492,13 +483,13 @@ def reconcile_vouchers(bank_transaction_name: str, vouchers: str):
@frappe.whitelist() @frappe.whitelist()
def get_linked_payments( def get_linked_payments(
bank_transaction_name: str, bank_transaction_name,
document_types: str | list[str] | None = None, document_types=None,
from_date: str | date | None = None, from_date=None,
to_date: str | date | None = None, to_date=None,
filter_by_reference_date: bool | None = None, filter_by_reference_date=None,
from_reference_date: bool | None = None, from_reference_date=None,
to_reference_date: str | None = None, to_reference_date=None,
): ):
# get all matching payments for a bank transaction # get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -675,7 +666,7 @@ def get_matching_queries(
queries.append(query) queries.append(query)
if transaction.deposit > 0.0 and "sales_invoice" in document_types: 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) queries.append(query)
if transaction.withdrawal > 0.0: if transaction.withdrawal > 0.0:
@@ -863,14 +854,11 @@ def get_je_matching_query(
return query 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 # get matching sales invoice query
si = frappe.qb.DocType("Sales Invoice") si = frappe.qb.DocType("Sales Invoice")
sip = frappe.qb.DocType("Sales Invoice Payment") 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_equality = sip.amount == common_filters.amount
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0) amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
amount_condition = amount_equality if exact_match else sip.amount > 0.0 amount_condition = amount_equality if exact_match else sip.amount > 0.0
@@ -883,11 +871,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.join(si) .join(si)
.on(sip.parent == si.name) .on(sip.parent == si.name)
.select( .select(
(ref_rank + party_rank + amount_rank + 1).as_("rank"), (party_rank + amount_rank + 1).as_("rank"),
ConstantColumn("Sales Invoice").as_("doctype"), ConstantColumn("Sales Invoice").as_("doctype"),
si.name, si.name,
sip.amount.as_("paid_amount"), sip.amount.as_("paid_amount"),
sip.reference_no, ConstantColumn("").as_("reference_no"),
ConstantColumn("").as_("reference_date"), ConstantColumn("").as_("reference_date"),
si.customer.as_("party"), si.customer.as_("party"),
ConstantColumn("Customer").as_("party_type"), ConstantColumn("Customer").as_("party_type"),
@@ -901,9 +889,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.where(si.currency == currency) .where(si.currency == currency)
) )
if frappe.flags.auto_reconcile_vouchers is True:
query = query.where(ref_condition)
return query return query

View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@@ -12,10 +13,9 @@ from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool
) )
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin): class TestBankReconciliationTool(AccountsTestMixin, IntegrationTestCase):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
@@ -24,6 +24,9 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run() qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account() self.create_bank_account()
def tearDown(self):
frappe.db.rollback()
def create_bank_account(self): def create_bank_account(self):
bank = frappe.get_doc( bank = frappe.get_doc(
{ {
@@ -40,7 +43,6 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
"bank": bank.name, "bank": bank.name,
"is_company_account": True, "is_company_account": True,
"account": self.bank, # account from Chart of Accounts "account": self.bank, # account from Chart of Accounts
"company": self.company,
} }
) )
.insert() .insert()

View File

@@ -76,6 +76,18 @@ class BankStatementImport(DataImport):
self.validate_google_sheets_url() self.validate_google_sheets_url()
def start_import(self): def start_import(self):
"""
Start a background import job for this Bank Statement Import.
Validates that the preview contains a "Bank Account" column and that the scheduler is active (unless running in test or developer mode). If validation passes and there is not already an enqueued job for this document, enqueue a background worker to perform the import.
Returns:
str | None: The enqueued job_id when a new job was queued, otherwise None.
Raises:
frappe.ValidationError: If the preview is missing a "Bank Account" column.
frappe.ValidationError: If the scheduler is inactive and import is not allowed to run immediately.
"""
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template( preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
self.import_file, self.google_sheets_url self.import_file, self.google_sheets_url
) )
@@ -112,24 +124,46 @@ class BankStatementImport(DataImport):
def preprocess_mt940_content(content: str) -> str: def preprocess_mt940_content(content: str) -> str:
"""Preprocess MT940 content to fix statement number format issues. """
Truncate overly long MT940 statement numbers found in `:28C:` tags to the last 5 digits.
The MT940 standard expects statement numbers to be maximum 5 digits,
but some banks provide longer statement numbers that cause parsing errors. This function fixes MT940 files where banks supply statement numbers longer than the MT940-expected maximum (5 digits),
This function truncates statement numbers longer than 5 digits to the last 5 digits. which can break parsers. It only processes lines that start with the `:28C:` tag and:
- leaves content unchanged if no `:28C:` tag is present,
- truncates numeric statement numbers longer than 5 digits to their last 5 digits,
- preserves any `/sequence` suffix and trailing whitespace on the same line.
Parameters:
content (str): Raw MT940 file content.
Returns:
str: The processed content with corrected `:28C:` statement numbers.
""" """
# Fast-path: bail if no :28C: tag exists # Fast-path: bail if no :28C: tag exists
if ":28C:" not in content: if ":28C:" not in content:
return content return content
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace # Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$") pattern = re.compile(r'(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$')
def replace_statement_number(match): def replace_statement_number(match):
"""
Replace a matched MT940 :28C: statement number by truncating it to the last five digits if it is longer.
Parameters:
match (re.Match): A regex match with groups:
1: prefix (e.g., ':28C:')
2: numeric statement number
3: optional sequence part (e.g., '/1')
4: optional trailing whitespace
Returns:
str: Reconstructed replacement string preserving prefix, (possibly truncated) statement number, sequence part, and trailing whitespace.
"""
prefix = match.group(1) # ':28C:' prefix = match.group(1) # ':28C:'
statement_num = match.group(2) # The statement number statement_num = match.group(2) # The statement number
sequence_part = match.group(3) or "" # The sequence part like '/1' sequence_part = match.group(3) or '' # The sequence part like '/1'
trailing_space = match.group(4) or "" # Preserve trailing whitespace trailing_space = match.group(4) or '' # Preserve trailing whitespace
# If statement number is longer than 5 digits, truncate to last 5 digits # If statement number is longer than 5 digits, truncate to last 5 digits
if len(statement_num) > 5: if len(statement_num) > 5:
@@ -143,10 +177,28 @@ def preprocess_mt940_content(content: str) -> str:
@frappe.whitelist() @frappe.whitelist()
def convert_mt940_to_csv(data_import: str, mt940_file_path: str): def convert_mt940_to_csv(data_import, mt940_file_path):
"""
Convert an MT940 file to a CSV and save it to the Frappe File Manager, returning the saved file URL.
This function:
- Loads the specified MT940 file, verifies it is MT940 format, preprocesses content to fix statement number formatting, and parses transactions.
- Writes parsed transactions to an in-memory CSV with headers: Date, Deposit, Withdrawal, Description, Reference Number, Bank Account, Currency.
- Saves the CSV as a private attachment on the Bank Statement Import document and returns the file URL.
Parameters:
data_import (str): Name (docname) of the Bank Statement Import document to attach the converted CSV to.
mt940_file_path (str): File path or file identifier pointing to the uploaded MT940 file to convert.
Returns:
str: URL of the saved CSV file in the File Manager.
Raises:
frappe.ValidationError: If the file is not MT940, MT940 import is not enabled on the document, parsing fails, or no transactions are found.
"""
doc = frappe.get_doc("Bank Statement Import", data_import) doc = frappe.get_doc("Bank Statement Import", data_import)
_file_doc, content = get_file(mt940_file_path) file_doc, content = get_file(mt940_file_path)
is_mt940 = is_mt940_format(content) is_mt940 = is_mt940_format(content)
if not is_mt940: if not is_mt940:
@@ -208,28 +260,26 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
@frappe.whitelist() @frappe.whitelist()
def get_preview_from_template( def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
data_import: str, import_file: str | None = None, google_sheets_url: str | None = None
):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template( return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url import_file, google_sheets_url
) )
@frappe.whitelist() @frappe.whitelist()
def form_start_import(data_import: str): def form_start_import(data_import):
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import() job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None return job_id is not None
@frappe.whitelist() @frappe.whitelist()
def download_errored_template(data_import_name: str): def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
@frappe.whitelist() @frappe.whitelist()
def download_import_log(data_import_name: str): def download_import_log(data_import_name):
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log() return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
@@ -285,7 +335,20 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
def update_mapping_db(bank, template_options): def update_mapping_db(bank, template_options):
"""Update bank transaction mapping database with template options.""" """
Update a Bank document's transaction field mappings to match the provided template options.
This replaces all existing entries in the Bank.bank_transaction_mapping child table with mappings from
the JSON-encoded template_options. The expected template_options JSON contains a "column_to_field_map"
object mapping file column names (keys) to bank transaction field names (values).
Parameters:
bank (str | frappe.model.document.Document): Bank name/docname or a Bank document.
template_options (str): JSON string containing a "column_to_field_map" mapping of file column -> bank field.
Side effects:
Overwrites the Bank.bank_transaction_mapping entries and saves the Bank document.
"""
bank = frappe.get_doc("Bank", bank) bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping: for d in bank.bank_transaction_mapping:
d.delete() d.delete()
@@ -297,7 +360,17 @@ def update_mapping_db(bank, template_options):
def add_bank_account(data, bank_account): def add_bank_account(data, bank_account):
"""Add bank account information to data rows.""" """
Ensure every data row contains the given bank account value.
Assumes `data` is a list of rows where data[0] is the header row. If the header row does not contain "Bank Account",
this function appends that header and appends the `bank_account` value to each subsequent row. If the header exists,
it sets the `bank_account` value into the existing "Bank Account" column for every data row. Mutates `data` in place.
Parameters:
data (list[list]): Table-like data with the first row as headers.
bank_account (str): Bank account value to set for each data row.
"""
bank_account_loc = None bank_account_loc = None
if "Bank Account" not in data[0]: if "Bank Account" not in data[0]:
data[0].append("Bank Account") data[0].append("Bank Account")
@@ -314,7 +387,21 @@ def add_bank_account(data, bank_account):
def write_files(import_file, data): def write_files(import_file, data):
"""Write processed data to CSV or Excel files.""" """
Write processed tabular data back to the original import file path (CSV or Excel).
This function overwrites the file referenced by import_file.file_doc.get_full_path().
- If the file extension is "csv", writes rows using the csv writer (expects `data` as an iterable of row iterables).
- If the extension is "xlsx" or "xls", writes to an Excel workbook using write_xlsx with sheet name "trans".
Parameters:
import_file: object
File wrapper whose `.file_doc.get_full_path()` and `.file_doc.get_extension()` are used to determine the target path and extension.
data: Iterable[Iterable]
Sequence of rows (each row is an iterable of cell values) to be written.
No return value.
"""
full_file_path = import_file.file_doc.get_full_path() full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension() parts = import_file.file_doc.get_extension()
extension = parts[1] extension = parts[1]
@@ -329,7 +416,21 @@ def write_files(import_file, data):
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
"""Write data to Excel file with formatting.""" """
Write rows of data to an Excel worksheet and save the workbook.
Creates a sheet named `sheet_name` in the provided openpyxl workbook (or a new write-only workbook if `wb` is None), applies optional column widths, converts HTML in string cells (except for sheets named "Data Import Template" or "Data Export"), strips characters illegal in Excel, and saves the workbook to `file_path`.
Parameters:
data (Iterable[Sequence]): Iterable of rows, where each row is a sequence of cell values.
sheet_name (str): Name of the worksheet to create.
wb (openpyxl.Workbook, optional): Workbook to append the sheet to. If not provided, a new write-only Workbook is created.
column_widths (Sequence[Number], optional): Sequence of column widths; indexes correspond to columns starting at 1.
file_path (str): File path where the workbook will be saved.
Returns:
bool: True on successful save.
"""
# from xlsx utils with changes # from xlsx utils with changes
column_widths = column_widths or [] column_widths = column_widths or []
if wb is None: if wb is None:
@@ -365,7 +466,7 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
@frappe.whitelist() @frappe.whitelist()
def get_import_status(docname: str): def get_import_status(docname):
import_status = {} import_status = {}
data_import = frappe.get_doc("Bank Statement Import", docname) data_import = frappe.get_doc("Bank Statement Import", docname)
@@ -373,7 +474,7 @@ def get_import_status(docname: str):
logs = frappe.get_all( logs = frappe.get_all(
"Data Import Log", "Data Import Log",
fields=[{"COUNT": "*", "as": "count"}, "success"], fields=["count(*) as count", "success"],
filters={"data_import": docname}, filters={"data_import": docname},
group_by="success", group_by="success",
) )

View File

@@ -1,14 +1,15 @@
# Copyright (c) 2020, Frappe Technologies and Contributors # Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt # See license.txt
import unittest
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import ( from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
is_mt940_format,
preprocess_mt940_content, preprocess_mt940_content,
is_mt940_format,
) )
from erpnext.tests.utils import ERPNextTestSuite
class TestBankStatementImport(ERPNextTestSuite): class TestBankStatementImport(unittest.TestCase):
"""Unit tests for Bank Statement Import functions""" """Unit tests for Bank Statement Import functions"""
def test_preprocess_mt940_content_with_long_statement_number(self): def test_preprocess_mt940_content_with_long_statement_number(self):
@@ -107,7 +108,14 @@ class TestBankStatementImport(ERPNextTestSuite):
self.assertFalse(is_mt940_format("")) self.assertFalse(is_mt940_format(""))
def test_preprocess_mt940_content_boundary_conditions(self): def test_preprocess_mt940_content_boundary_conditions(self):
"""Test boundary conditions for statement number length""" """
Verify preprocessing handles statement-number length boundaries in `:28C:` tags.
Checks that:
- A 6-digit statement number is truncated to its last 5 digits.
- A 5-digit statement number remains unchanged.
- A very long statement number is reduced to its last 5 digits.
"""
# Test exactly 6 digits (should be truncated) # Test exactly 6 digits (should be truncated)
mt940_content = ":28C:123456/1" mt940_content = ":28C:123456/1"
expected_content = ":28C:23456/1" expected_content = ":28C:23456/1"
@@ -126,7 +134,11 @@ class TestBankStatementImport(ERPNextTestSuite):
self.assertEqual(result, expected_content) self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_real_world_case(self): def test_preprocess_mt940_content_real_world_case(self):
"""Test with real-world MT940 content that was failing in production""" """
Verify preprocessing of a real-world MT940 document: truncate 6-digit `:28C:` statement numbers to their last 5 digits and preserve all other content.
Uses a sanitized, production-failing MT940 sample where `:28C:167619/1` must become `:28C:67619/1`. Asserts the entire document matches the expected transformed output, that the truncated tag is present and the original is absent, and that unrelated fields (e.g., `:20:` reference and UPI details) remain unchanged.
"""
# This is based on actual MT940 content that was causing parsing errors (sanitized) # This is based on actual MT940 content that was causing parsing errors (sanitized)
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4: mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619 :20:STMTREF167619

View File

@@ -38,10 +38,7 @@
"column_break_3czf", "column_break_3czf",
"bank_party_name", "bank_party_name",
"bank_party_account_number", "bank_party_account_number",
"bank_party_iban", "bank_party_iban"
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
], ],
"fields": [ "fields": [
{ {
@@ -119,14 +116,15 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "reference_number", "fieldname": "reference_number",
"fieldtype": "Small Text", "fieldtype": "Data",
"label": "Reference Number" "label": "Reference Number"
}, },
{ {
"fieldname": "transaction_id", "fieldname": "transaction_id",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Transaction ID", "label": "Transaction ID",
"read_only": 1 "read_only": 1,
"unique": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -236,32 +234,12 @@
{ {
"fieldname": "column_break_oufv", "fieldname": "column_break_oufv",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-12-07 20:49:18.600757", "modified": "2025-08-29 11:53:45.908169",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",

View File

@@ -32,13 +32,11 @@ class BankTransaction(Document):
date: DF.Date | None date: DF.Date | None
deposit: DF.Currency deposit: DF.Currency
description: DF.SmallText | None description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"] naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None party: DF.DynamicLink | None
party_type: DF.Link | None party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments] payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.SmallText | None reference_number: DF.Data | None
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"] status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
transaction_id: DF.Data | None transaction_id: DF.Data | None
transaction_type: DF.Data | None transaction_type: DF.Data | None
@@ -47,14 +45,9 @@ class BankTransaction(Document):
# end: auto-generated types # end: auto-generated types
def before_validate(self): def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount() self.update_allocated_amount()
def on_discard(self):
self.db_set("status", "Cancelled")
def validate(self): def validate(self):
self.validate_included_fee()
self.validate_duplicate_references() self.validate_duplicate_references()
self.validate_currency() self.validate_currency()
@@ -139,8 +132,6 @@ class BankTransaction(Document):
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry) self.delink_payment_entry(payment_entry)
@@ -316,40 +307,6 @@ class BankTransaction(Document):
self.party_type, self.party = result self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist() @frappe.whitelist()
def get_doctypes_for_bank_reconciliation(): def get_doctypes_for_bank_reconciliation():
@@ -375,12 +332,11 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"), ("unallocated_amount", "bank_account"),
as_dict=True, as_dict=True,
) )
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt_bank_account != gl_bank_account: if bt.bank_account != gl_bank_account:
frappe.throw( frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format( _("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt_bank_account, payment_entry.payment_entry, gl_bank_account bt.bank_account, payment_entry.payment_entry, gl_bank_account
) )
) )

View File

@@ -35,7 +35,7 @@ def upload_bank_statement():
@frappe.whitelist() @frappe.whitelist()
def create_bank_entries(columns: str, data: str, bank_account: str): def create_bank_entries(columns, data, bank_account):
header_map = get_header_mapping(columns, bank_account) header_map = get_header_mapping(columns, bank_account)
success = 0 success = 0

View File

@@ -2,20 +2,27 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.tests.utils import ERPNextTestSuite
IBAN_1 = "DE02000000003716541159" IBAN_1 = "DE02000000003716541159"
IBAN_2 = "DE02500105170137075030" IBAN_2 = "DE02500105170137075030"
class TestAutoMatchParty(ERPNextTestSuite): class TestAutoMatchParty(IntegrationTestCase):
def setUp(self): @classmethod
def setUpClass(cls):
create_bank_account() create_bank_account()
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1) frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1) frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
return super().setUpClass()
@classmethod
def tearDownClass(cls):
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self): def test_match_by_account_number(self):
create_supplier_for_match(account_no=IBAN_1[11:]) create_supplier_for_match(account_no=IBAN_1[11:])

View File

@@ -6,6 +6,7 @@ import json
import frappe import frappe
from frappe import utils from frappe import utils
from frappe.model.docstatus import DocStatus from frappe.model.docstatus import DocStatus
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
get_linked_payments, get_linked_payments,
@@ -18,10 +19,12 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_paymen
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile 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 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed from erpnext.tests.utils import if_lending_app_installed
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item", "Cost Center"]
class TestBankTransaction(ERPNextTestSuite): class TestBankTransaction(IntegrationTestCase):
def setUp(self): def setUp(self):
make_pos_profile() make_pos_profile()
@@ -382,7 +385,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_group": "Individual", "customer_group": "All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Poore Simon's", "customer_name": "Poore Simon's",
} }
@@ -413,7 +416,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Customer", "doctype": "Customer",
"customer_group": "Individual", "customer_group": "All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Fayva", "customer_name": "Fayva",
} }

View File

@@ -1,134 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.tests.utils import ERPNextTestSuite
class TestBankTransactionFees(ERPNextTestSuite):
def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 101
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
def test_included_fee_allows_equal(self):
"""A fee that's part of a withdrawal may be equal to the withdrawal
amount (only the fee was deducted from the account)."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 100
bt.validate_included_fee()
def test_included_fee_allows_for_deposit(self):
"""For deposits, a fee may be recorded separately without limiting the
received amount."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.included_fee = 999
bt.validate_included_fee()
def test_excluded_fee_noop_when_zero(self):
"""When there is no excluded fee to apply, the amounts should remain
unchanged."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 5
bt.excluded_fee = 0
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 100)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_throws_when_exceeds_deposit(self):
"""A fee deducted from an incoming payment must not exceed the incoming
amount (else it would be a withdrawal, a conversion we don't support)."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.excluded_fee = 11
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
"""A transaction must be either incoming or outgoing when applying a
fee, not both."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.withdrawal = 10
bt.excluded_fee = 1
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_deducts_from_deposit(self):
"""When a fee is deducted from an incoming payment, the net received
amount decreases and the fee is tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 95)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
"""A separately-deducted fee may reduce an incoming payment to zero,
while still tracking the fee."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 5
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_increases_outgoing_payment(self):
"""When a separately-deducted fee is provided for an outgoing payment,
the total money leaving increases and the fee is tracked."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 100
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 105)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
"""If only an excluded fee is provided, it should be treated as an
outgoing payment and the fee is then tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 5)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)

View File

@@ -2,10 +2,8 @@
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests import IntegrationTestCase
from erpnext.tests.utils import ERPNextTestSuite class TestBisectAccountingStatements(IntegrationTestCase):
class TestBisectAccountingStatements(ERPNextTestSuite):
pass pass

View File

@@ -2,10 +2,8 @@
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests import IntegrationTestCase
from erpnext.tests.utils import ERPNextTestSuite class TestBisectNodes(IntegrationTestCase):
class TestBisectNodes(ERPNextTestSuite):
pass pass

View File

@@ -4,6 +4,16 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Budget", { frappe.ui.form.on("Budget", {
onload: function (frm) { 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 () { frm.set_query("monthly_distribution", function () {
return { return {
filters: { filters: {
@@ -12,37 +22,16 @@ frappe.ui.form.on("Budget", {
}; };
}); });
frm.set_query("account", function () {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => { frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
if (value) { if (!value) {
frm.get_field("control_action_for_cumulative_expense_section").hide(); frm.get_field("control_action_for_cumulative_expense_section").hide();
} }
}); });
}, },
refresh: async function (frm) { refresh: function (frm) {
frm.trigger("toggle_reqd_fields"); frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
}
toggle_distribution_fields(frm);
}, },
budget_against: function (frm) { budget_against: function (frm) {
@@ -50,20 +39,6 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields"); 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);
});
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
distribute_equally: function (frm) {
toggle_distribution_fields(frm);
},
set_null_value: function (frm) { set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") { if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null); frm.set_value("project", null);
@@ -76,68 +51,4 @@ frappe.ui.form.on("Budget", {
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center"); frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
frm.toggle_reqd("project", frm.doc.budget_against == "Project"); 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);
set_total_budget_amount(frm);
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);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
});
function set_total_budget_amount(frm) {
let total = 0;
(frm.doc.budget_distribution || []).forEach((row) => {
total += flt(row.amount);
});
frm.set_value("budget_distribution_total", total);
}
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});
grid.refresh();
}

View File

@@ -12,23 +12,10 @@
"company", "company",
"cost_center", "cost_center",
"project", "project",
"account", "fiscal_year",
"column_break_3", "column_break_3",
"monthly_distribution",
"amended_from", "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_wkqb",
"column_break_paum",
"column_break_nwor",
"budget_distribution_total",
"section_break_6", "section_break_6",
"applicable_on_material_request", "applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr", "action_if_annual_budget_exceeded_on_mr",
@@ -45,8 +32,8 @@
"applicable_on_cumulative_expense", "applicable_on_cumulative_expense",
"action_if_annual_exceeded_on_cumulative_expense", "action_if_annual_exceeded_on_cumulative_expense",
"action_if_accumulated_monthly_exceeded_on_cumulative_expense", "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
"section_break_kkan", "section_break_21",
"revision_of" "accounts"
], ],
"fields": [ "fields": [
{ {
@@ -57,7 +44,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Budget Against", "label": "Budget Against",
"options": "\nCost Center\nProject", "options": "\nCost Center\nProject",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -67,7 +53,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -77,8 +62,7 @@
"in_global_search": 1, "in_global_search": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center", "options": "Cost Center"
"read_only_depends_on": "eval: doc.revision_of"
}, },
{ {
"depends_on": "eval:doc.budget_against == 'Project'", "depends_on": "eval:doc.budget_against == 'Project'",
@@ -86,13 +70,28 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Project", "label": "Project",
"options": "Project", "options": "Project"
"read_only_depends_on": "eval: doc.revision_of" },
{
"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", "fieldname": "column_break_3",
"fieldtype": "Column Break" "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", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@@ -188,12 +187,22 @@
"options": "\nStop\nWarn\nIgnore" "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", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"no_copy": 1, "no_copy": 1,
"options": "BUDGET-.########", "options": "BUDGET-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1
@@ -223,117 +232,13 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense", "label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
"options": "\nStop\nWarn\nIgnore" "options": "\nStop\nWarn\nIgnore"
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"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
},
{
"fieldname": "section_break_wkqb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_paum",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_nwor",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_distribution_total",
"fieldtype": "Currency",
"label": "Budget Distribution Total",
"no_copy": 1,
"read_only": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-12-10 02:35:01.197613", "modified": "2025-06-16 15:57:13.114981",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",

View File

@@ -5,9 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
from frappe.utils.data import get_first_day
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -32,9 +30,9 @@ class Budget(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF 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: 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_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
@@ -49,118 +47,73 @@ class Budget(Document):
applicable_on_material_request: DF.Check applicable_on_material_request: DF.Check
applicable_on_purchase_order: DF.Check applicable_on_purchase_order: DF.Check
budget_against: DF.Literal["", "Cost Center", "Project"] budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_distribution_total: DF.Currency
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link company: DF.Link
cost_center: DF.Link | None cost_center: DF.Link | None
distribute_equally: DF.Check fiscal_year: DF.Link
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"] monthly_distribution: DF.Link | None
from_fiscal_year: DF.Link naming_series: DF.Literal["BUDGET-.YYYY.-"]
naming_series: DF.Literal["BUDGET-.########"]
project: DF.Link | None project: DF.Link | None
revision_of: DF.Data | None
to_fiscal_year: DF.Link
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
if not self.get(frappe.scrub(self.budget_against)): if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against)) frappe.throw(_("{0} is mandatory").format(self.budget_against))
self.validate_budget_amount()
self.validate_fiscal_year()
self.set_fiscal_year_dates()
self.validate_duplicate() self.validate_duplicate()
self.validate_account() self.validate_accounts()
self.set_null_value() self.set_null_value()
self.validate_applicable_for() 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): def validate_duplicate(self):
budget_against_field = frappe.scrub(self.budget_against) budget_against_field = frappe.scrub(self.budget_against)
budget_against = self.get(budget_against_field) 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( existing_budget = frappe.db.sql(
f""" """
SELECT name, account select
FROM `tabBudget` b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
WHERE where
docstatus < 2 ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
AND company = %s b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
AND {budget_against_field} = %s "%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
AND account = %s ),
AND name != %s (self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
AND ( as_dict=1,
(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,
) )
if existing_budget: for d in existing_budget:
d = existing_budget[0]
frappe.throw( frappe.throw(
_( _(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years." "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), ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
DuplicateBudgetError, DuplicateBudgetError,
) )
def validate_account(self): def validate_accounts(self):
if not self.account: account_list = []
frappe.throw(_("Account is mandatory")) for d in self.get("accounts"):
if d.account:
account_details = frappe.get_cached_value( account_details = frappe.get_cached_value(
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1 "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(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
) )
)
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): def set_null_value(self):
if self.budget_against == "Cost Center": if self.budget_against == "Cost Center":
@@ -186,232 +139,30 @@ class Budget(Document):
): ):
self.applicable_on_booking_actual_expenses = 1 self.applicable_on_booking_actual_expenses = 1
def validate_existing_expenses(self):
if self.is_new() and self.revision_of:
return
params = frappe._dict( def validate_expense_against_budget(args, expense_amount=0):
{ args = frappe._dict(args)
"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()
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self._should_skip_allocation():
return
if self._should_recalculate_manual_distribution():
self._recalculate_manual_distribution()
return
if not self.should_regenerate_budget_distribution():
return
self._regenerate_distribution()
def _should_skip_allocation(self):
return self.revision_of and not self.distribute_equally
def _should_recalculate_manual_distribution(self):
return (
not self.distribute_equally
and bool(self.budget_distribution)
and self._is_only_budget_amount_changed()
)
def _is_only_budget_amount_changed(self):
old = self.get_doc_before_save()
if not old:
return False
return (
old.budget_amount != self.budget_amount
and old.distribution_frequency == self.distribution_frequency
and old.budget_start_date == self.budget_start_date
and old.budget_end_date == self.budget_end_date
)
def _recalculate_manual_distribution(self):
for row in self.budget_distribution:
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
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",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
return True
return bool(self.distribute_equally)
def _regenerate_distribution(self):
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)
self.budget_distribution_total = self.budget_amount
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):
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 validate_expense_against_budget(params, expense_amount=0):
params = frappe._dict(params)
if not frappe.db.count("Budget", cache=True): if not frappe.db.count("Budget", cache=True):
return return
if not params.fiscal_year: if not args.fiscal_year:
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0] args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
posting_date = getdate(params.get("posting_date")) if args.get("company"):
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"):
frappe.flags.exception_approver_role = frappe.get_cached_value( 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: if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
params.account = params.get("expense_account") return
if not params.get("expense_account") and params.get("account"): if not args.account:
params.expense_account = params.account args.account = args.get("expense_account")
if not (params.get("account") and params.get("cost_center")) and params.item_code: if not (args.get("account") and args.get("cost_center")) and args.item_code:
params.cost_center, params.account = get_item_details(params) args.cost_center, args.account = get_item_details(args)
if not params.account: if not args.account:
return return
default_dimensions = [ default_dimensions = [
@@ -429,78 +180,59 @@ def validate_expense_against_budget(params, expense_amount=0):
budget_against = dimension.get("fieldname") budget_against = dimension.get("fieldname")
if ( if (
params.get(budget_against) args.get(budget_against)
and params.account and args.account
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense") and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
): ):
doctype = dimension.get("document_type") doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"): 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}` condition = f"""and exists(select name from `tab{doctype}`
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
params.is_tree = True args.is_tree = True
else: else:
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}" condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
params.is_tree = False args.is_tree = False
params.budget_against_field = budget_against args.budget_against_field = budget_against
params.budget_against_doctype = doctype args.budget_against_doctype = doctype
budget_records = frappe.db.sql( budget_records = frappe.db.sql(
f""" f"""
SELECT select
b.name, b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
b.{budget_against} AS budget_against, ifnull(b.applicable_on_material_request, 0) as for_material_request,
b.budget_amount, ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
b.from_fiscal_year, ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
b.to_fiscal_year, b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
b.budget_start_date, b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.budget_end_date, b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
IFNULL(b.applicable_on_material_request, 0) AS for_material_request, from
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order, `tabBudget` b, `tabBudget Account` ba
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses, where
b.action_if_annual_budget_exceeded, b.name=ba.parent and b.fiscal_year=%s
b.action_if_accumulated_monthly_budget_exceeded, and ba.account=%s and b.docstatus=1
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
{condition} {condition}
""", """,
(params.company, params.posting_date, params.account), (args.fiscal_year, args.account),
as_dict=True, as_dict=True,
) # nosec ) # nosec
if budget_records: 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: for budget in budget_records:
if flt(budget.budget_amount): if flt(budget.budget_amount):
yearly_action, monthly_action = get_actions(params, budget) yearly_action, monthly_action = get_actions(args, budget)
params["for_material_request"] = budget.for_material_request args["for_material_request"] = budget.for_material_request
params["for_purchase_order"] = budget.for_purchase_order args["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,
)
if yearly_action in ("Stop", "Warn"): if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget( compare_expense_with_budget(
params, args,
flt(budget.budget_amount), flt(budget.budget_amount),
_("Annual"), _("Annual"),
yearly_action, yearly_action,
@@ -509,12 +241,14 @@ def validate_budget_records(params, budget_records, expense_amount):
) )
if monthly_action in ["Stop", "Warn"]: 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( compare_expense_with_budget(
params, args,
budget_amount, budget_amount,
_("Accumulated Monthly"), _("Accumulated Monthly"),
monthly_action, monthly_action,
@@ -523,41 +257,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): def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0 args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
if not amount: if not amount:
params.requested_amount, params.ordered_amount = ( args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
get_requested_amount(params),
get_ordered_amount(params),
)
if params.get("doctype") == "Material Request" and params.for_material_request: if args.get("doctype") == "Material Request" and args.for_material_request:
amount = params.requested_amount + params.ordered_amount amount = args.requested_amount + args.ordered_amount
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order: elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
amount = params.ordered_amount amount = args.ordered_amount
total_expense = params.actual_expense + amount total_expense = args.actual_expense + amount
if total_expense > budget_amount: if total_expense > budget_amount:
if params.actual_expense > budget_amount: if args.actual_expense > budget_amount:
diff = params.actual_expense - budget_amount error_tense = _("is already")
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.") diff = args.actual_expense - budget_amount
else: else:
error_tense = _("will be")
diff = total_expense - budget_amount 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") currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _msg.format(
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
_(action_for), _(action_for),
frappe.bold(params.account), frappe.bold(args.account),
frappe.unscrub(params.budget_against_field), frappe.unscrub(args.budget_against_field),
frappe.bold(budget_against), frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(budget_amount, currency=currency)),
error_tense,
frappe.bold(fmt_money(diff, currency=currency)), 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( if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
frappe.session.user frappe.session.user
@@ -570,25 +303,14 @@ def compare_expense_with_budget(params, budget_amount, action_for, action, budge
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_expense_breakup(params, currency, budget_against): def get_expense_breakup(args, currency, budget_against):
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through")) msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
common_filters = frappe._dict( common_filters = frappe._dict(
{ {
params.budget_against_field: budget_against, args.budget_against_field: budget_against,
"account": params.account, "account": args.account,
"company": params.company, "company": args.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,
} }
) )
@@ -597,23 +319,18 @@ def get_expense_breakup(params, currency, budget_against):
+ frappe.utils.get_link_to_report( + frappe.utils.get_link_to_report(
"General Ledger", "General Ledger",
label=_("Actual Expenses"), label=_("Actual Expenses"),
filters=gl_filters, 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>" + "</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 += ( msg += (
"<li>" "<li>"
@@ -622,24 +339,22 @@ def get_expense_breakup(params, currency, budget_against):
label=_("Material Requests"), label=_("Material Requests"),
report_type="Report Builder", report_type="Report Builder",
doctype="Material Request", 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>" + "</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 += ( msg += (
"<li>" "<li>"
+ frappe.utils.get_link_to_report( + frappe.utils.get_link_to_report(
@@ -647,34 +362,42 @@ def get_expense_breakup(params, currency, budget_against):
label=_("Unbilled Orders"), label=_("Unbilled Orders"),
report_type="Report Builder", report_type="Report Builder",
doctype="Purchase Order", 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>" + "</li></ul>"
) )
return msg return msg
def get_actions(params, budget): def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
if 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 yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
elif 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 yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action return yearly_action, monthly_action
def get_requested_amount(params): def get_requested_amount(args):
item_code = params.get("item_code") item_code = args.get("item_code")
condition = get_other_condition(params, "Material Request") condition = get_other_condition(args, "Material Request")
data = frappe.db.sql( data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
@@ -688,9 +411,9 @@ def get_requested_amount(params):
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_ordered_amount(params): def get_ordered_amount(args):
item_code = params.get("item_code") item_code = args.get("item_code")
condition = get_other_condition(params, "Purchase Order") condition = get_other_condition(args, "Purchase Order")
data = frappe.db.sql( data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -704,102 +427,111 @@ def get_ordered_amount(params):
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_other_condition(params, for_doc): def get_other_condition(args, for_doc):
condition = f"expense_account = '{params.expense_account}'" condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = params.get("budget_against_field") budget_against_field = args.get("budget_against_field")
if budget_against_field and params.get(budget_against_field): if budget_against_field and args.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{params.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") condition += f""" and parent.{date_field}
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date") between '{start_date}' and '{end_date}' """
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
return condition return condition
def get_actual_expense(params): def get_actual_expense(args):
if not params.budget_against_doctype: if not args.budget_against_doctype:
params.budget_against_doctype = frappe.unscrub(params.budget_against_field) args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
budget_against_field = params.get("budget_against_field") budget_against_field = args.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else "" condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
date_condition = ( if args.is_tree:
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
)
if params.is_tree:
lft_rgt = frappe.db.get_value( 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""" args.update(lft_rgt)
and exists(
select name from `tab{params.budget_against_doctype}` condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
where lft >= %(lft)s and rgt <= %(rgt)s where lft>=%(lft)s and rgt<=%(rgt)s
and name = gle.{budget_against_field} and name=gle.{budget_against_field})"""
)
"""
else: else:
condition2 = f""" condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
and gle.{budget_against_field} = %({budget_against_field})s where name=gle.{budget_against_field} and
""" gle.{budget_against_field} = %({budget_against_field})s)"""
amount = flt( amount = flt(
frappe.db.sql( frappe.db.sql(
f""" f"""
select sum(gle.debit) - sum(gle.credit) select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle from `tabGL Entry` gle
where where
is_cancelled = 0 is_cancelled = 0
and gle.account = %(account)s and gle.account=%(account)s
{condition1} {condition1}
{date_condition} and gle.fiscal_year=%(fiscal_year)s
and gle.company = %(company)s and gle.company=%(company)s
and gle.docstatus = 1 and gle.docstatus=1
{condition2} {condition2}
""", """,
params, (args),
)[0][0] )[0][0]
) # nosec ) # nosec
return amount return amount
def get_accumulated_monthly_budget(budget_name, posting_date): def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
posting_date = getdate(posting_date) distribution = {}
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
bd = frappe.qb.DocType("Budget Distribution") res = (
b = frappe.qb.DocType("Budget") 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 = ( for d in res:
frappe.qb.from_(bd) distribution.setdefault(d.month, d.percentage_allocation)
.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)
)
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 and 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 cost_center, expense_account = None, None
if not params.get("company"): if not args.get("company"):
return cost_center, expense_account return cost_center, expense_account
if params.item_code: if args.item_code:
item_defaults = frappe.db.get_value( item_defaults = frappe.db.get_value(
"Item Default", "Item Default",
{"parent": params.item_code, "company": params.get("company")}, {"parent": args.item_code, "company": args.get("company")},
["buying_cost_center", "expense_account"], ["buying_cost_center", "expense_account"],
) )
if item_defaults: if item_defaults:
@@ -807,7 +539,7 @@ def get_item_details(params):
if not (cost_center and expense_account): if not (cost_center and expense_account):
for doctype in ["Item Group", "Company"]: for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, params) data = get_expense_cost_center(doctype, args)
if not cost_center and data: if not cost_center and data:
cost_center = data[0] cost_center = data[0]
@@ -821,39 +553,14 @@ def get_item_details(params):
return cost_center, expense_account return cost_center, expense_account
def get_expense_cost_center(doctype, params): def get_expense_cost_center(doctype, args):
if doctype == "Item Group": if doctype == "Item Group":
return frappe.db.get_value( return frappe.db.get_value(
"Item Default", "Item Default",
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")}, {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
["buying_cost_center", "expense_account"], ["buying_cost_center", "expense_account"],
) )
else: else:
return frappe.db.get_value( return frappe.db.get_value(
doctype, 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: str):
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

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