mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-27 12:58:34 +00:00
Compare commits
1 Commits
v16.3.0
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a28dfcd44c |
@@ -45,9 +45,3 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
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
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
|
||||
|
||||
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
|
||||
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
|
||||
|
||||
### Reply and Closing Policy
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -9,7 +9,7 @@ body:
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
|
||||
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
|
||||
2. When making a bug report, make sure you provide all required information. The easier it is for
|
||||
maintainers to reproduce, the faster it'll be fixed.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forum
|
||||
url: https://discuss.frappe.io/c/erpnext/6
|
||||
url: https://discuss.erpnext.com/
|
||||
about: For general QnA, discussions and community help.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ assignees: ''
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
|
||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||
the original discussion.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
|
||||
|
||||
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
||||
1. Certified ERPNext partners: https://erpnext.com/partners
|
||||
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
|
||||
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
|
||||
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
||||
|
||||
-->
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
||||
4
.github/workflows/docs-checker.yml
vendored
4
.github/workflows/docs-checker.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Docs
|
||||
env:
|
||||
|
||||
6
.github/workflows/generate-pot-file.yml
vendored
6
.github/workflows/generate-pot-file.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
|
||||
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15", "16"]
|
||||
version: ["14", "15"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Download Semgrep rules
|
||||
|
||||
26
.github/workflows/patch.yml
vendored
26
.github/workflows/patch.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:11.8
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -49,17 +49,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: |
|
||||
3.11
|
||||
3.13
|
||||
3.14
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -88,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
@@ -113,8 +110,8 @@ jobs:
|
||||
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
|
||||
|
||||
wget https://frappe.io/files/erpnext-v14.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
|
||||
wget https://erpnext.com/files/v13-erpnext.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/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
@@ -135,14 +132,15 @@ jobs:
|
||||
# Resetup env and install apps
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env --python python$2
|
||||
bench -v setup env
|
||||
bench pip install -e ./apps/erpnext
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 15 3.13
|
||||
update_to_version 14
|
||||
update_to_version 15
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-16
|
||||
- version-13
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup dependencies
|
||||
|
||||
14
.github/workflows/run-indinvidual-tests.yml
vendored
14
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- id: set-matrix
|
||||
run: |
|
||||
# Use grep and find to get the list of test files
|
||||
@@ -72,17 +72,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
4
.github/workflows/semantic-commits.yml
vendored
4
.github/workflows/semantic-commits.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
name: Check Commit Titles
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 200
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
20
.github/workflows/server-tests-mariadb.yml
vendored
20
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.svg'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
@@ -63,12 +62,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -79,9 +78,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -110,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
@@ -129,9 +128,10 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
||||
env:
|
||||
TYPE: server
|
||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
|
||||
- name: Show bench output
|
||||
@@ -140,6 +140,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
@@ -148,9 +149,10 @@ jobs:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
12
.github/workflows/server-tests-postgres.yml
vendored
12
.github/workflows/server-tests-postgres.yml
vendored
@@ -47,12 +47,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -50,15 +50,6 @@ pull_request_rules:
|
||||
- version-15-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: backport to version-16-beta
|
||||
conditions:
|
||||
- label="backport version-16-beta"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-16-beta
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"branches": ["version-16"],
|
||||
"branches": ["version-13"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
@@ -21,4 +21,4 @@
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ To setup the repository locally follow the steps mentioned below:
|
||||
|
||||
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.
|
||||
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
|
||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.3.0"
|
||||
__version__ = "16.0.0-dev"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -9,20 +9,18 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2026-01-02 13:01:24.037552",
|
||||
"modified": "2026-01-02 13:04:57.850305",
|
||||
"last_synced_on": "2020-07-22 12:19:59.879476",
|
||||
"modified": "2020-07-22 12:21:48.780513",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Balance",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"source": "Account Balance Timeline",
|
||||
"time_interval": "Monthly",
|
||||
"timeseries": 1,
|
||||
"time_interval": "Quarterly",
|
||||
"timeseries": 0,
|
||||
"timespan": "Last Year",
|
||||
"type": "Line",
|
||||
"use_report_chart": 0,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"chart_type": "Report",
|
||||
"creation": "2025-04-01 20:38:16.986176",
|
||||
"creation": "2020-07-17 11:25:34.448572",
|
||||
"docstatus": 0,
|
||||
"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()\"}",
|
||||
@@ -9,7 +9,7 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2025-12-19 12:37:31.673782",
|
||||
"modified": "2023-07-19 13:08:56.470390",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss",
|
||||
@@ -17,9 +17,8 @@
|
||||
"owner": "Administrator",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"timeseries": 0,
|
||||
"type": "Line",
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
@@ -47,8 +46,7 @@ def validate_service_stop_date(doc):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date
|
||||
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
|
||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
||||
):
|
||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||
|
||||
@@ -319,7 +317,7 @@ def get_already_booked_amount(doc, item):
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
@@ -450,12 +448,14 @@ def process_deferred_accounting(posting_date=None):
|
||||
for company in companies:
|
||||
for record_type in ("Income", "Expense"):
|
||||
doc = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
)
|
||||
)
|
||||
|
||||
doc.insert()
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"account_currency",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_category",
|
||||
"account_type",
|
||||
"tax_rate",
|
||||
"freeze_account",
|
||||
@@ -190,20 +189,13 @@
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable"
|
||||
},
|
||||
{
|
||||
"description": "Used with Financial Report Template",
|
||||
"fieldname": "account_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Category",
|
||||
"options": "Account Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-02 06:26:44.657146",
|
||||
"modified": "2025-01-22 10:40:35.766017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -258,7 +250,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_number",
|
||||
"show_name_in_global_search": 1,
|
||||
"show_preview_popup": 1,
|
||||
@@ -266,4 +257,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ class Account(NestedSet):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
account_name: DF.Data
|
||||
account_number: DF.Data | None
|
||||
@@ -93,10 +92,8 @@ class Account(NestedSet):
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
|
||||
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -111,7 +108,6 @@ class Account(NestedSet):
|
||||
self.validate_parent_child_account_type()
|
||||
self.validate_root_details()
|
||||
self.validate_account_number()
|
||||
self.validate_disabled()
|
||||
self.validate_group_or_ledger()
|
||||
self.set_root_and_report_type()
|
||||
self.validate_mandatory()
|
||||
@@ -256,14 +252,6 @@ class Account(NestedSet):
|
||||
|
||||
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
|
||||
|
||||
def validate_disabled(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.disabled) == cint(self.disabled):
|
||||
return
|
||||
|
||||
if cint(self.disabled):
|
||||
self.validate_default_accounts_in_company()
|
||||
|
||||
def validate_group_or_ledger(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
|
||||
@@ -274,41 +262,18 @@ class Account(NestedSet):
|
||||
elif cint(self.is_group):
|
||||
if self.account_type and not self.flags.exclude_account_type_check:
|
||||
throw(_("Cannot covert to Group because Account Type is selected."))
|
||||
self.validate_default_accounts_in_company()
|
||||
elif self.check_if_child_exists():
|
||||
throw(_("Account with child nodes cannot be set as ledger"))
|
||||
|
||||
def validate_default_accounts_in_company(self):
|
||||
default_account_fields = get_company_default_account_fields()
|
||||
|
||||
company_default_accounts = frappe.db.get_value(
|
||||
"Company", self.company, list(default_account_fields.keys()), as_dict=1
|
||||
)
|
||||
|
||||
msg = _("Account {0} cannot be disabled as it is already set as {1} for {2}.")
|
||||
|
||||
if not self.disabled:
|
||||
msg = _("Account {0} cannot be converted to Group as it is already set as {1} for {2}.")
|
||||
|
||||
for d in default_account_fields:
|
||||
if company_default_accounts.get(d) == self.name:
|
||||
throw(
|
||||
msg.format(
|
||||
frappe.bold(self.name),
|
||||
frappe.bold(default_account_fields.get(d)),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_frozen_accounts_modifier(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||
return
|
||||
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
||||
throw(_("You are not authorized to set Frozen value"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
@@ -660,27 +625,3 @@ def _ensure_idle_system():
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
|
||||
def get_company_default_account_fields():
|
||||
return {
|
||||
"default_bank_account": "Default Bank Account",
|
||||
"default_cash_account": "Default Cash Account",
|
||||
"default_receivable_account": "Default Receivable Account",
|
||||
"default_payable_account": "Default Payable Account",
|
||||
"default_expense_account": "Default Expense Account",
|
||||
"default_income_account": "Default Income Account",
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
|
||||
"stock_adjustment_account": "Stock Adjustment Account",
|
||||
"write_off_account": "Write Off Account",
|
||||
"default_discount_account": "Default Payment Discount Account",
|
||||
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
|
||||
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
|
||||
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
|
||||
"round_off_account": "Round Off Account",
|
||||
"default_deferred_revenue_account": "Default Deferred Revenue Account",
|
||||
"default_deferred_expense_account": "Default Deferred Expense Account",
|
||||
"accumulated_depreciation_account": "Accumulated Depreciation Account",
|
||||
"depreciation_expense_account": "Depreciation Expense Account",
|
||||
"disposal_account": "Gain/Loss Account on Asset Disposal",
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ frappe.treeview_settings["Account"] = {
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -161,14 +160,6 @@ frappe.treeview_settings["Account"] = {
|
||||
.options,
|
||||
description: __("Optional. This setting will be used to filter in various transactions."),
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "account_category",
|
||||
label: __("Account Category"),
|
||||
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
|
||||
.options,
|
||||
description: __("Optional. Used with Financial Report Template"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "tax_rate",
|
||||
|
||||
@@ -23,7 +23,15 @@ def create_charts(
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
if account_name not in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
@@ -47,7 +55,6 @@ def create_charts(
|
||||
"report_type": report_type,
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_category": child.get("account_category"),
|
||||
"account_currency": child.get("account_currency")
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
@@ -90,7 +97,20 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(set(child.keys()) - set(get_chart_metadata_fields())):
|
||||
elif len(
|
||||
set(child.keys())
|
||||
- set(
|
||||
[
|
||||
"account_name",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
]
|
||||
)
|
||||
):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
@@ -233,7 +253,13 @@ def validate_bank_account(coa, bank_account):
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
if account_name not in [
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
@@ -258,7 +284,15 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"""recursively called to form a parent-child based list of dict from chart template"""
|
||||
for account_name, child in children.items():
|
||||
account = {}
|
||||
if account_name in get_chart_metadata_fields():
|
||||
if account_name in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
continue
|
||||
|
||||
if from_coa_importer:
|
||||
@@ -276,16 +310,3 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
|
||||
_import_accounts(chart, None)
|
||||
return accounts
|
||||
|
||||
|
||||
def get_chart_metadata_fields():
|
||||
return [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"account_category",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,192 +9,103 @@ def get():
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"}
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {"account_type": "Cash", "account_category": "Cash and Cash Equivalents"},
|
||||
"account_type": "Cash",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
_("Employee Advances"): {"account_type": "Payable"},
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {"account_category": "Other Current Assets"}
|
||||
},
|
||||
_("Prepaid Expenses"): {"account_category": "Other Current Assets"},
|
||||
_("Short-term Investments"): {"account_category": "Short-term Investments"},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
|
||||
_("Stock In Hand"): {"account_type": "Stock"},
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_category": "Other Current Assets"},
|
||||
_("Tax Assets"): {"is_group": 1},
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_category": "Intangible Assets"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset"},
|
||||
_("Software"): {"account_type": "Fixed Asset"},
|
||||
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_category": "Long-term Investments"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_category": "Other Non-current Assets",
|
||||
}
|
||||
},
|
||||
_("Investments"): {"is_group": 1},
|
||||
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
|
||||
"root_type": "Asset",
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_category": "Other Direct Costs",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
|
||||
},
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Commission on Sales"): {"account_category": "Operating Expenses"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"},
|
||||
_("Entertainment Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Office Rent"): {"account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {"account_category": "Operating Expenses"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"},
|
||||
_("Salary"): {"account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
|
||||
_("Impairment"): {"account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_category": "Tax Expense"},
|
||||
_("Administrative Expenses"): {},
|
||||
_("Commission on Sales"): {},
|
||||
_("Depreciation"): {"account_type": "Depreciation"},
|
||||
_("Entertainment Expenses"): {},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
|
||||
_("Legal Expenses"): {},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable"},
|
||||
_("Office Maintenance Expenses"): {},
|
||||
_("Office Rent"): {},
|
||||
_("Postal Expenses"): {},
|
||||
_("Print and Stationery"): {},
|
||||
_("Round Off"): {"account_type": "Round Off"},
|
||||
_("Salary"): {},
|
||||
_("Sales Expenses"): {},
|
||||
_("Telephone Expenses"): {},
|
||||
_("Travel Expenses"): {},
|
||||
_("Utility Expenses"): {},
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
_("Impairment"): {},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_category": "Revenue from Operations"},
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
|
||||
"is_group": 1,
|
||||
},
|
||||
_("Direct Income"): {_("Sales"): {}, _("Service"): {}},
|
||||
_("Indirect Income"): {"is_group": 1},
|
||||
"root_type": "Income",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"},
|
||||
_("Payroll Payable"): {"account_category": "Other Payables"},
|
||||
_("Creditors"): {"account_type": "Payable"},
|
||||
_("Payroll Payable"): {},
|
||||
},
|
||||
_("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
|
||||
_("Customer Advances"): {"account_category": "Other Current Liabilities"},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
|
||||
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {"account_category": "Short-term Provisions"},
|
||||
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Unsecured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"},
|
||||
_("Secured Loans"): {},
|
||||
_("Unsecured Loans"): {},
|
||||
_("Bank Overdraft Account"): {},
|
||||
},
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {"account_category": "Long-term Provisions"},
|
||||
_("Employee Benefits Obligation"): {"account_category": "Other Non-current Liabilities"},
|
||||
"is_group": 1,
|
||||
},
|
||||
"root_type": "Liability",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Capital Stock"): {"account_type": "Equity"},
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,128 +10,49 @@ def get():
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {
|
||||
"account_type": "Receivable",
|
||||
"account_number": "1310",
|
||||
"account_category": "Trade Receivables",
|
||||
},
|
||||
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
|
||||
"account_number": "1300",
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_number": "1200",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {
|
||||
"account_type": "Cash",
|
||||
"account_number": "1110",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash"): {"account_type": "Cash", "account_number": "1110"},
|
||||
"account_type": "Cash",
|
||||
"account_number": "1100",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {
|
||||
"account_number": "1651",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Earnest Money"): {"account_number": "1651"},
|
||||
"account_number": "1650",
|
||||
},
|
||||
_("Prepaid Expenses"): {
|
||||
"account_number": "1660",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Short-term Investments"): {
|
||||
"account_number": "1670",
|
||||
"account_category": "Short-term Investments",
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {
|
||||
"account_type": "Stock",
|
||||
"account_number": "1410",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1500",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_number": "1500"},
|
||||
"account_number": "1100-1600",
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1710",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1720",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1730",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1740",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1750",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1760",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Software"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1770",
|
||||
"account_category": "Intangible Assets",
|
||||
},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_number": "1790",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
|
||||
"account_number": "1700",
|
||||
},
|
||||
_("Investments"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1800",
|
||||
"account_category": "Long-term Investments",
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_number": "1800"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_number": "1910",
|
||||
"account_category": "Other Non-current Assets",
|
||||
},
|
||||
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
|
||||
"account_number": "1900",
|
||||
},
|
||||
"root_type": "Asset",
|
||||
@@ -140,94 +61,42 @@ def get():
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_number": "5111",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_number": "5112",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_number": "5118",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_number": "5119",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
|
||||
"account_number": "5110",
|
||||
},
|
||||
"account_number": "5100",
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {
|
||||
"account_number": "5201",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Commission on Sales"): {
|
||||
"account_number": "5202",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Depreciation"): {
|
||||
"account_type": "Depreciation",
|
||||
"account_number": "5203",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Entertainment Expenses"): {
|
||||
"account_number": "5204",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5205",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_number": "5206", "account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5207",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {
|
||||
"account_number": "5208",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Rent"): {"account_number": "5209", "account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_number": "5210", "account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {
|
||||
"account_number": "5211",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Round Off"): {
|
||||
"account_type": "Round Off",
|
||||
"account_number": "5212",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Salary"): {"account_number": "5213", "account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_number": "5214", "account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_number": "5215", "account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_number": "5216", "account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_number": "5217", "account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_number": "5218", "account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219", "account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_number": "5220", "account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_number": "5221", "account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {
|
||||
"account_number": "5222",
|
||||
"account_category": "Other Operating Income",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5223",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
|
||||
_("Administrative Expenses"): {"account_number": "5201"},
|
||||
_("Commission on Sales"): {"account_number": "5202"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
|
||||
_("Entertainment Expenses"): {"account_number": "5204"},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
|
||||
_("Legal Expenses"): {"account_number": "5206"},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
|
||||
_("Office Maintenance Expenses"): {"account_number": "5208"},
|
||||
_("Office Rent"): {"account_number": "5209"},
|
||||
_("Postal Expenses"): {"account_number": "5210"},
|
||||
_("Print and Stationery"): {"account_number": "5211"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
|
||||
_("Salary"): {"account_number": "5213"},
|
||||
_("Sales Expenses"): {"account_number": "5214"},
|
||||
_("Telephone Expenses"): {"account_number": "5215"},
|
||||
_("Travel Expenses"): {"account_number": "5216"},
|
||||
_("Utility Expenses"): {"account_number": "5217"},
|
||||
_("Write Off"): {"account_number": "5218"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
@@ -235,126 +104,54 @@ def get():
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_number": "4110", "account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_number": "4120", "account_category": "Revenue from Operations"},
|
||||
_("Sales"): {"account_number": "4110"},
|
||||
_("Service"): {"account_number": "4120"},
|
||||
"account_number": "4100",
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_number": "4210", "account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {
|
||||
"account_number": "4220",
|
||||
"account_category": "Investment Income",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "4200",
|
||||
},
|
||||
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
|
||||
"root_type": "Income",
|
||||
"account_number": "4000",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {
|
||||
"account_type": "Payable",
|
||||
"account_number": "2110",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Payroll Payable"): {"account_number": "2120", "account_category": "Other Payables"},
|
||||
_("Creditors"): {"account_type": "Payable", "account_number": "2110"},
|
||||
_("Payroll Payable"): {"account_number": "2120"},
|
||||
"account_number": "2100",
|
||||
},
|
||||
_("Accrued Expenses"): {
|
||||
"account_number": "2150",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Customer Advances"): {
|
||||
"account_number": "2160",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "2210",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_number": "2211",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
"account_number": "2200",
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
_("TDS Payable"): {
|
||||
"account_number": "2310",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("TDS Payable"): {"account_number": "2310"},
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_number": "2300",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {
|
||||
"account_number": "2350",
|
||||
"account_category": "Short-term Provisions",
|
||||
},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {
|
||||
"account_number": "2410",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Unsecured Loans"): {
|
||||
"account_number": "2420",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Bank Overdraft Account"): {
|
||||
"account_number": "2430",
|
||||
"account_category": "Short-term Borrowings",
|
||||
},
|
||||
_("Secured Loans"): {"account_number": "2410"},
|
||||
_("Unsecured Loans"): {"account_number": "2420"},
|
||||
_("Bank Overdraft Account"): {"account_number": "2430"},
|
||||
"account_number": "2400",
|
||||
},
|
||||
"account_number": "2100-2400",
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {
|
||||
"account_number": "2510",
|
||||
"account_category": "Long-term Provisions",
|
||||
},
|
||||
_("Employee Benefits Obligation"): {
|
||||
"account_number": "2520",
|
||||
"account_category": "Other Non-current Liabilities",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "2500",
|
||||
},
|
||||
"root_type": "Liability",
|
||||
"account_number": "2000",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3100",
|
||||
"account_category": "Share Capital",
|
||||
},
|
||||
_("Dividends Paid"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3200",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3300",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3400",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Revaluation Surplus"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3500",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
|
||||
"root_type": "Equity",
|
||||
"account_number": "3000",
|
||||
},
|
||||
|
||||
@@ -415,13 +415,15 @@ def create_account(**kwargs):
|
||||
return account.name
|
||||
else:
|
||||
account = frappe.get_doc(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
dict(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
)
|
||||
|
||||
account.save()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Account Category", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:account_category_name",
|
||||
"creation": "2025-08-02 06:22:31.835063",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_category_name",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account_category_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Account Category Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-15 03:19:47.171349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_category_name, description",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document, bulk_insert
|
||||
|
||||
DOCTYPE = "Account Category"
|
||||
|
||||
|
||||
class AccountCategory(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category_name: DF.Data
|
||||
description: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FormulaFieldUpdater,
|
||||
)
|
||||
|
||||
# get all template rows with this account category being used
|
||||
row = frappe.qb.DocType("Financial Report Row")
|
||||
rows = frappe._dict(
|
||||
frappe.qb.from_(row)
|
||||
.select(row.name, row.calculation_formula)
|
||||
.where(row.calculation_formula.like(f"%{old_name}%"))
|
||||
.run()
|
||||
)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# Update formulas with new name
|
||||
updater = FormulaFieldUpdater(
|
||||
field_name="account_category",
|
||||
value_mapping={old_name: new_name},
|
||||
exclude_operators=["like", "not like"],
|
||||
)
|
||||
|
||||
updated_formulas = updater.update_in_rows(rows)
|
||||
|
||||
if updated_formulas:
|
||||
frappe.msgprint(
|
||||
_("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
|
||||
)
|
||||
|
||||
|
||||
def import_account_categories(template_path: str):
|
||||
categories_file = os.path.join(template_path, "account_categories.json")
|
||||
|
||||
if not os.path.exists(categories_file):
|
||||
return
|
||||
|
||||
with open(categories_file) as f:
|
||||
categories = json.load(f, object_hook=frappe._dict)
|
||||
|
||||
create_account_categories(categories)
|
||||
|
||||
|
||||
def create_account_categories(categories: list[dict]):
|
||||
if not categories:
|
||||
return
|
||||
|
||||
existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
|
||||
new_categories = []
|
||||
|
||||
for category_data in categories:
|
||||
category_name = category_data.get("account_category_name")
|
||||
if not category_name or category_name in existing_categories:
|
||||
continue
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
**category_data,
|
||||
"doctype": DOCTYPE,
|
||||
"name": category_name,
|
||||
}
|
||||
)
|
||||
|
||||
new_categories.append(doc)
|
||||
existing_categories.add(category_name)
|
||||
|
||||
if new_categories:
|
||||
bulk_insert(DOCTYPE, new_categories)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestAccountCategory(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for AccountCategory.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"exempted_role",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -51,13 +49,6 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -68,22 +59,13 @@
|
||||
"label": "Closed Documents",
|
||||
"options": "Closed Document",
|
||||
"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": [],
|
||||
"modified": "2025-12-01 16:53:44.631299",
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -123,9 +105,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,7 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
exempted_role: DF.Link | None
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
# end: auto-generated types
|
||||
@@ -114,11 +112,10 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
accounting_period = (
|
||||
frappe.qb.from_(ap)
|
||||
.from_(cd)
|
||||
.select(ap.name, ap.exempted_role)
|
||||
.select(ap.name)
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
@@ -127,11 +124,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
).run(as_dict=1)
|
||||
|
||||
if accounting_period:
|
||||
if (
|
||||
accounting_period[0].get("exempted_role")
|
||||
and accounting_period[0].get("exempted_role") in frappe.get_roles()
|
||||
):
|
||||
return
|
||||
frappe.throw(
|
||||
_("You cannot create a {0} within the closed Accounting Period {1}").format(
|
||||
doc.doctype, frappe.bold(accounting_period[0]["name"])
|
||||
|
||||
@@ -37,59 +37,6 @@ class TestAccountingPeriod(IntegrationTestCase):
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def test_accounting_period_exempted_role(self):
|
||||
# Create Accounting Period with exempted role
|
||||
ap = create_accounting_period(
|
||||
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 tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
@@ -104,6 +51,5 @@ def create_accounting_period(**args):
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
accounting_period.exempted_role = args.exempted_role or ""
|
||||
|
||||
return accounting_period
|
||||
|
||||
@@ -64,17 +64,26 @@
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"role_to_notify_on_depreciation_failure",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
@@ -82,23 +91,31 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"tab_break_dpet",
|
||||
"chart_of_accounts_section",
|
||||
"show_balance_in_coa",
|
||||
"banking_section",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"payment_request_section",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_section",
|
||||
"use_legacy_budget_controller"
|
||||
"budget_settings",
|
||||
"use_new_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -279,9 +296,16 @@
|
||||
"fieldname": "column_break_19",
|
||||
"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",
|
||||
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -319,6 +343,11 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -333,6 +362,11 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
@@ -343,7 +377,7 @@
|
||||
{
|
||||
"fieldname": "tab_break_dpet",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Others"
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -387,6 +421,11 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
@@ -462,9 +501,14 @@
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
@@ -506,6 +550,10 @@
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
@@ -545,6 +593,17 @@
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"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",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -592,61 +651,15 @@
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -22,6 +23,7 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
@@ -40,7 +42,6 @@ class AccountsSettings(Document):
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
@@ -49,6 +50,7 @@ class AccountsSettings(Document):
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
@@ -57,11 +59,11 @@ class AccountsSettings(Document):
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
@@ -72,8 +74,7 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
use_legacy_controller_for_pcv: DF.Check
|
||||
use_new_budget_controller: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -98,6 +99,9 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -124,6 +128,10 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
@@ -152,5 +160,6 @@ class AccountsSettings(Document):
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
refresh(frm) {
|
||||
frm.set_currency_labels(["amount"], frm.doc.currency);
|
||||
frm.set_currency_labels(["base_amount"], erpnext.get_currency(frm.doc.company));
|
||||
},
|
||||
});
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"amount",
|
||||
"base_amount",
|
||||
"currency",
|
||||
"event",
|
||||
"delinked"
|
||||
],
|
||||
@@ -78,29 +76,13 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "base_amount",
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_rate",
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"precision": "9",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-13 12:45:03.014555",
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
|
||||
@@ -19,12 +19,10 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
base_amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
exchange_rate: DF.Float
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
@@ -36,15 +34,3 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["against_voucher_type", "against_voucher_no"],
|
||||
)
|
||||
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["voucher_type", "voucher_no"],
|
||||
)
|
||||
|
||||
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal file
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal 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": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BudgetDistribution(Document):
|
||||
class AdvanceTax(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -14,13 +14,14 @@ class BudgetDistribution(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
end_date: DF.Date | None
|
||||
account_head: DF.Link | None
|
||||
allocated_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
percent: DF.Percent
|
||||
start_date: DF.Date | None
|
||||
reference_detail: DF.Data | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -14,7 +14,6 @@
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"is_tax_withholding_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -26,6 +25,7 @@
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"allocated_amount",
|
||||
"column_break_13",
|
||||
"base_tax_amount",
|
||||
"base_net_amount",
|
||||
@@ -97,11 +97,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -172,6 +172,12 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
@@ -207,26 +213,18 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:42:18.707671",
|
||||
"modified": "2024-11-22 19:16:22.346267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class AdvanceTaxesandCharges(Document):
|
||||
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
allocated_amount: DF.Currency
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -27,12 +28,10 @@ class AdvanceTaxesandCharges(Document):
|
||||
currency: DF.Link | None
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
frappe.provide("erpnext.integrations");
|
||||
|
||||
frappe.ui.form.on("Bank", {
|
||||
onload: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
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;
|
||||
|
||||
if (grid) {
|
||||
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||
}
|
||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
||||
"bank_transaction_field",
|
||||
"options",
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
console.error(error);
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -99,7 +98,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -254,7 +252,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -51,29 +51,25 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_is_company_account()
|
||||
self.validate_company()
|
||||
self.validate_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):
|
||||
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.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
if self.account:
|
||||
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.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):
|
||||
if self.is_default and not self.disabled:
|
||||
|
||||
@@ -125,7 +125,7 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
msg += "</ul>"
|
||||
msgprint(_(msg))
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
@@ -134,44 +134,16 @@ class BankClearance(Document):
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
old_clearance_date = frappe.db.get_value(
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{
|
||||
"parent": d.payment_entry,
|
||||
"account": self.account,
|
||||
"amount": [">", 0],
|
||||
},
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"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:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
old_clearance_date = payment_entry.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
|
||||
),
|
||||
)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
@@ -183,10 +155,8 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -211,20 +181,19 @@ def get_payment_entries_for_bank_clearance(
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
posting_date ASC, name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"label": "Payment Entry",
|
||||
"oldfieldname": "voucher_id",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "payment_document"
|
||||
"options": "payment_document",
|
||||
"width": "50"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
@@ -68,7 +69,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "cheque_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@@ -78,10 +79,8 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "cheque_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Cheque Date",
|
||||
"oldfieldname": "cheque_date",
|
||||
"oldfieldtype": "Date",
|
||||
@@ -97,19 +96,17 @@
|
||||
"oldfieldtype": "Date"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-17 14:33:45.913311",
|
||||
"modified": "2024-03-27 13:06:37.609319",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Clearance Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("bank", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -304,7 +304,6 @@ def create_payment_entry_bts(
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
company_bank_account=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
@@ -346,9 +345,6 @@ def create_payment_entry_bts(
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
if company_bank_account:
|
||||
pe.bank_account = company_bank_account
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
@@ -413,7 +409,7 @@ def start_auto_reconcile(
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
["payment_entry", "journal_entry"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -670,7 +666,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -858,14 +854,11 @@ def get_je_matching_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
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -878,11 +871,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -896,9 +889,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import openpyxl
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.data_import import DataImport
|
||||
from frappe.core.doctype.data_import.importer import Importer, ImportFile
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.file_manager import get_file, save_file
|
||||
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
@@ -112,54 +111,20 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""Preprocess MT940 content to fix statement number format issues.
|
||||
|
||||
The MT940 standard expects statement numbers to be maximum 5 digits,
|
||||
but some banks provide longer statement numbers that cause parsing errors.
|
||||
This function truncates statement numbers longer than 5 digits to the last 5 digits.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
|
||||
|
||||
def replace_statement_number(match):
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or "" # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or "" # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
if not is_mt940_format(content):
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
transactions = mt940.parse(content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
@@ -284,7 +249,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""Update bank transaction mapping database with template options."""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -296,7 +260,6 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""Add bank account information to data rows."""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -313,7 +276,6 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""Write processed data to CSV or Excel files."""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -323,12 +285,11 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension in ("xlsx", "xls"):
|
||||
elif extension == "xlsx" or "xls":
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""Write data to Excel file with formatting."""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
@@ -372,7 +333,7 @@ def get_import_status(docname):
|
||||
|
||||
logs = frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=[{"COUNT": "*", "as": "count"}, "success"],
|
||||
fields=["count(*) as count", "success"],
|
||||
filters={"data_import": docname},
|
||||
group_by="success",
|
||||
)
|
||||
|
||||
@@ -1,209 +1,10 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""Test boundary conditions for statement number length"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""Test with real-world MT940 content that was failing in production"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
@@ -38,10 +38,7 @@
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban",
|
||||
"extended_bank_statement_section",
|
||||
"included_fee",
|
||||
"excluded_fee"
|
||||
"bank_party_iban"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -119,14 +116,15 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction ID",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -236,32 +234,12 @@
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-07 20:49:18.600757",
|
||||
"modified": "2025-08-29 11:53:45.908169",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,13 +32,11 @@ class BankTransaction(Document):
|
||||
date: DF.Date | None
|
||||
deposit: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
excluded_fee: DF.Currency
|
||||
included_fee: DF.Currency
|
||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.SmallText | None
|
||||
reference_number: DF.Data | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
@@ -47,14 +45,9 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -314,40 +307,6 @@ class BankTransaction(Document):
|
||||
|
||||
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()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(UnitTestCase):
|
||||
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)
|
||||
@@ -4,6 +4,16 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
frappe.ui.form.on("Budget", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("account", "accounts", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("monthly_distribution", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -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);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
|
||||
if (!value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: async function (frm) {
|
||||
refresh: function (frm) {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
budget_against: function (frm) {
|
||||
@@ -50,20 +39,6 @@ frappe.ui.form.on("Budget", {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
},
|
||||
|
||||
budget_amount(frm) {
|
||||
if (frm.doc.budget_distribution?.length) {
|
||||
frm.doc.budget_distribution.forEach((row) => {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
});
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
|
||||
distribute_equally: function (frm) {
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
set_null_value: function (frm) {
|
||||
if (frm.doc.budget_against == "Cost Center") {
|
||||
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("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();
|
||||
}
|
||||
|
||||
@@ -12,23 +12,10 @@
|
||||
"company",
|
||||
"cost_center",
|
||||
"project",
|
||||
"account",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
"monthly_distribution",
|
||||
"amended_from",
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_start_date",
|
||||
"budget_end_date",
|
||||
"distribution_frequency",
|
||||
"budget_amount",
|
||||
"section_break_nwug",
|
||||
"distribute_equally",
|
||||
"section_break_fpdt",
|
||||
"budget_distribution",
|
||||
"section_break_wkqb",
|
||||
"column_break_paum",
|
||||
"column_break_nwor",
|
||||
"budget_distribution_total",
|
||||
"section_break_6",
|
||||
"applicable_on_material_request",
|
||||
"action_if_annual_budget_exceeded_on_mr",
|
||||
@@ -45,8 +32,8 @@
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_kkan",
|
||||
"revision_of"
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -57,7 +44,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Budget Against",
|
||||
"options": "\nCost Center\nProject",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -67,7 +53,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -77,8 +62,7 @@
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.budget_against == 'Project'",
|
||||
@@ -86,13 +70,28 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
|
||||
"fieldname": "monthly_distribution",
|
||||
"fieldtype": "Link",
|
||||
"label": "Monthly Distribution",
|
||||
"options": "Monthly Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -188,12 +187,22 @@
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"default": "BUDGET-.########",
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Accounts",
|
||||
"options": "Budget Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.########",
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
@@ -223,117 +232,13 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-10 02:35:01.197613",
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
|
||||
from frappe.utils.data import get_first_day, nowdate
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -34,9 +30,9 @@ class Budget(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
|
||||
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount
|
||||
|
||||
account: DF.Link
|
||||
accounts: DF.Table[BudgetAccount]
|
||||
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
@@ -51,118 +47,73 @@ class Budget(Document):
|
||||
applicable_on_material_request: DF.Check
|
||||
applicable_on_purchase_order: DF.Check
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
budget_amount: DF.Currency
|
||||
budget_distribution: DF.Table[BudgetDistribution]
|
||||
budget_distribution_total: DF.Currency
|
||||
budget_end_date: DF.Date | None
|
||||
budget_start_date: DF.Date | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
distribute_equally: DF.Check
|
||||
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
|
||||
from_fiscal_year: DF.Link
|
||||
naming_series: DF.Literal["BUDGET-.########"]
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
project: DF.Link | None
|
||||
revision_of: DF.Data | None
|
||||
to_fiscal_year: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
if not self.get(frappe.scrub(self.budget_against)):
|
||||
frappe.throw(_("{0} is mandatory").format(self.budget_against))
|
||||
self.validate_budget_amount()
|
||||
self.validate_fiscal_year()
|
||||
self.set_fiscal_year_dates()
|
||||
self.validate_duplicate()
|
||||
self.validate_account()
|
||||
self.validate_accounts()
|
||||
self.set_null_value()
|
||||
self.validate_applicable_for()
|
||||
self.validate_existing_expenses()
|
||||
|
||||
def validate_budget_amount(self):
|
||||
if self.budget_amount <= 0:
|
||||
frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount))
|
||||
|
||||
def validate_fiscal_year(self):
|
||||
if self.from_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.from_fiscal_year, self.company)
|
||||
if self.to_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.to_fiscal_year, self.company)
|
||||
|
||||
def validate_fiscal_year_company(self, fiscal_year, company):
|
||||
linked_companies = frappe.get_all(
|
||||
"Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company"
|
||||
)
|
||||
if linked_companies and company not in linked_companies:
|
||||
frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company))
|
||||
|
||||
def set_fiscal_year_dates(self):
|
||||
if self.from_fiscal_year:
|
||||
self.budget_start_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.from_fiscal_year, "year_start_date"
|
||||
)
|
||||
if self.to_fiscal_year:
|
||||
self.budget_end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.to_fiscal_year, "year_end_date"
|
||||
)
|
||||
|
||||
if self.budget_start_date > self.budget_end_date:
|
||||
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
|
||||
|
||||
def validate_duplicate(self):
|
||||
budget_against_field = frappe.scrub(self.budget_against)
|
||||
budget_against = self.get(budget_against_field)
|
||||
account = self.account
|
||||
|
||||
if not account:
|
||||
return
|
||||
|
||||
accounts = [d.account for d in self.accounts] or []
|
||||
existing_budget = frappe.db.sql(
|
||||
f"""
|
||||
SELECT name, account
|
||||
FROM `tabBudget`
|
||||
WHERE
|
||||
docstatus < 2
|
||||
AND company = %s
|
||||
AND {budget_against_field} = %s
|
||||
AND account = %s
|
||||
AND name != %s
|
||||
AND (
|
||||
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
)
|
||||
""",
|
||||
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
|
||||
as_dict=True,
|
||||
"""
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
|
||||
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
|
||||
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
|
||||
),
|
||||
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if existing_budget:
|
||||
d = existing_budget[0]
|
||||
for d in existing_budget:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years."
|
||||
).format(d.name, self.budget_against, budget_against, d.account),
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
|
||||
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
|
||||
DuplicateBudgetError,
|
||||
)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
frappe.throw(_("Account is mandatory"))
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
self.account
|
||||
def validate_accounts(self):
|
||||
account_list = []
|
||||
for d in self.get("accounts"):
|
||||
if d.account:
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
|
||||
else:
|
||||
account_list.append(d.account)
|
||||
|
||||
def set_null_value(self):
|
||||
if self.budget_against == "Cost Center":
|
||||
@@ -188,232 +139,30 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def validate_existing_expenses(self):
|
||||
if self.is_new() and self.revision_of:
|
||||
return
|
||||
|
||||
params = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"account": self.account,
|
||||
"budget_start_date": self.budget_start_date,
|
||||
"budget_end_date": self.budget_end_date,
|
||||
"budget_against_field": frappe.scrub(self.budget_against),
|
||||
"budget_against_doctype": frappe.unscrub(self.budget_against),
|
||||
}
|
||||
)
|
||||
|
||||
params[params.budget_against_field] = self.get(params.budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"):
|
||||
params.is_tree = True
|
||||
else:
|
||||
params.is_tree = False
|
||||
|
||||
actual_spent = get_actual_expense(params)
|
||||
|
||||
if actual_spent > self.budget_amount:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Spending for Account {0} ({1}) between {2} and {3} "
|
||||
"has already exceeded the new allocated budget. "
|
||||
"Spent: {4}, Budget: {5}"
|
||||
).format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
frappe.bold(self.budget_start_date),
|
||||
frappe.bold(self.budget_end_date),
|
||||
frappe.bold(frappe.utils.fmt_money(actual_spent)),
|
||||
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
|
||||
),
|
||||
title=_("Budget Limit Exceeded"),
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.allocate_budget()
|
||||
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)
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
return
|
||||
|
||||
if not params.fiscal_year:
|
||||
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
|
||||
if not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
|
||||
posting_date = getdate(params.get("posting_date"))
|
||||
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
|
||||
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
|
||||
|
||||
budget_exists = frappe.db.sql(
|
||||
"""
|
||||
select name
|
||||
from `tabBudget`
|
||||
where company = %s
|
||||
and docstatus = 1
|
||||
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
limit 1
|
||||
""",
|
||||
(params.company, year_end_date, year_start_date),
|
||||
)
|
||||
|
||||
if not budget_exists:
|
||||
return
|
||||
|
||||
if params.get("company"):
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", params.get("company"), "exception_budget_approver_role"
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not params.account:
|
||||
params.account = params.get("expense_account")
|
||||
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
|
||||
return
|
||||
|
||||
if not params.get("expense_account") and params.get("account"):
|
||||
params.expense_account = params.account
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
|
||||
if not (params.get("account") and params.get("cost_center")) and params.item_code:
|
||||
params.cost_center, params.account = get_item_details(params)
|
||||
if not (args.get("account") and args.get("cost_center")) and args.item_code:
|
||||
args.cost_center, args.account = get_item_details(args)
|
||||
|
||||
if not params.account:
|
||||
if not args.account:
|
||||
return
|
||||
|
||||
default_dimensions = [
|
||||
@@ -431,78 +180,59 @@ def validate_expense_against_budget(params, expense_amount=0):
|
||||
budget_against = dimension.get("fieldname")
|
||||
|
||||
if (
|
||||
params.get(budget_against)
|
||||
and params.account
|
||||
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||
):
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
|
||||
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
params.is_tree = True
|
||||
args.is_tree = True
|
||||
else:
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
|
||||
params.is_tree = False
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
|
||||
args.is_tree = False
|
||||
|
||||
params.budget_against_field = budget_against
|
||||
params.budget_against_doctype = doctype
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
b.name,
|
||||
b.{budget_against} AS budget_against,
|
||||
b.budget_amount,
|
||||
b.from_fiscal_year,
|
||||
b.to_fiscal_year,
|
||||
b.budget_start_date,
|
||||
b.budget_end_date,
|
||||
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
|
||||
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
|
||||
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded,
|
||||
b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
FROM
|
||||
`tabBudget` b
|
||||
WHERE
|
||||
b.company = %s
|
||||
AND b.docstatus = 1
|
||||
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
|
||||
AND b.account = %s
|
||||
select
|
||||
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
from
|
||||
`tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
{condition}
|
||||
""",
|
||||
(params.company, params.posting_date, params.account),
|
||||
""",
|
||||
(args.fiscal_year, args.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
if budget_records:
|
||||
validate_budget_records(params, budget_records, expense_amount)
|
||||
validate_budget_records(args, budget_records, expense_amount)
|
||||
|
||||
|
||||
def validate_budget_records(params, budget_records, expense_amount):
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
yearly_action, monthly_action = get_actions(params, budget)
|
||||
params["for_material_request"] = budget.for_material_request
|
||||
params["for_purchase_order"] = budget.for_purchase_order
|
||||
params["from_fiscal_year"], params["to_fiscal_year"] = (
|
||||
budget.from_fiscal_year,
|
||||
budget.to_fiscal_year,
|
||||
)
|
||||
params["budget_start_date"], params["budget_end_date"] = (
|
||||
budget.budget_start_date,
|
||||
budget.budget_end_date,
|
||||
)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
args["for_material_request"] = budget.for_material_request
|
||||
args["for_purchase_order"] = budget.for_purchase_order
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
params,
|
||||
args,
|
||||
flt(budget.budget_amount),
|
||||
_("Annual"),
|
||||
yearly_action,
|
||||
@@ -511,12 +241,14 @@ def validate_budget_records(params, budget_records, expense_amount):
|
||||
)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
|
||||
budget_amount = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
|
||||
)
|
||||
|
||||
params["month_end_date"] = get_last_day(params.posting_date)
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
|
||||
compare_expense_with_budget(
|
||||
params,
|
||||
args,
|
||||
budget_amount,
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
@@ -525,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):
|
||||
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||
if not amount:
|
||||
params.requested_amount, params.ordered_amount = (
|
||||
get_requested_amount(params),
|
||||
get_ordered_amount(params),
|
||||
)
|
||||
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||
|
||||
if params.get("doctype") == "Material Request" and params.for_material_request:
|
||||
amount = params.requested_amount + params.ordered_amount
|
||||
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||
amount = args.requested_amount + args.ordered_amount
|
||||
|
||||
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
|
||||
amount = params.ordered_amount
|
||||
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||
amount = args.ordered_amount
|
||||
|
||||
total_expense = params.actual_expense + amount
|
||||
total_expense = args.actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if params.actual_expense > budget_amount:
|
||||
diff = params.actual_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
|
||||
if args.actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = args.actual_expense - budget_amount
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
|
||||
|
||||
currency = frappe.get_cached_value("Company", params.company, "default_currency")
|
||||
msg = _msg.format(
|
||||
currency = frappe.get_cached_value("Company", args.company, "default_currency")
|
||||
|
||||
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
|
||||
_(action_for),
|
||||
frappe.bold(params.account),
|
||||
frappe.unscrub(params.budget_against_field),
|
||||
frappe.bold(args.account),
|
||||
frappe.unscrub(args.budget_against_field),
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
error_tense,
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
msg += get_expense_breakup(params, currency, budget_against)
|
||||
msg += get_expense_breakup(args, currency, budget_against)
|
||||
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
@@ -572,25 +303,14 @@ def compare_expense_with_budget(params, budget_amount, action_for, action, budge
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(params, currency, budget_against):
|
||||
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
params.budget_against_field: budget_against,
|
||||
"account": params.account,
|
||||
"company": params.company,
|
||||
}
|
||||
)
|
||||
|
||||
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
gl_filters = common_filters.copy()
|
||||
gl_filters.update(
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"is_cancelled": 0,
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -599,23 +319,18 @@ def get_expense_breakup(params, currency, budget_against):
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
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>"
|
||||
)
|
||||
mr_filters = common_filters.copy()
|
||||
mr_filters.update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
@@ -624,24 +339,22 @@ def get_expense_breakup(params, currency, budget_against):
|
||||
label=_("Material Requests"),
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=mr_filters,
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(params.requested_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
po_filters = common_filters.copy()
|
||||
po_filters.update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
@@ -649,34 +362,42 @@ def get_expense_breakup(params, currency, budget_against):
|
||||
label=_("Unbilled Orders"),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=po_filters,
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(params.ordered_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def get_actions(params, budget):
|
||||
def get_actions(args, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
|
||||
if params.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
|
||||
|
||||
elif params.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_po
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_requested_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Material Request")
|
||||
def get_requested_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
@@ -690,9 +411,9 @@ def get_requested_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Purchase Order")
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
@@ -706,102 +427,111 @@ def get_ordered_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(params, for_doc):
|
||||
condition = f"expense_account = '{params.expense_account}'"
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and params.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
start_date, end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
|
||||
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
def get_actual_expense(params):
|
||||
if not params.budget_against_doctype:
|
||||
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
|
||||
def get_actual_expense(args):
|
||||
if not args.budget_against_doctype:
|
||||
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
|
||||
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||
|
||||
date_condition = (
|
||||
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
|
||||
)
|
||||
|
||||
if params.is_tree:
|
||||
if args.is_tree:
|
||||
lft_rgt = frappe.db.get_value(
|
||||
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
params.update(lft_rgt)
|
||||
|
||||
condition2 = f"""
|
||||
and exists(
|
||||
select name from `tab{params.budget_against_doctype}`
|
||||
where lft >= %(lft)s and rgt <= %(rgt)s
|
||||
and name = gle.{budget_against_field}
|
||||
)
|
||||
"""
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})"""
|
||||
else:
|
||||
condition2 = f"""
|
||||
and gle.{budget_against_field} = %({budget_against_field})s
|
||||
"""
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account = %(account)s
|
||||
{condition1}
|
||||
{date_condition}
|
||||
and gle.company = %(company)s
|
||||
and gle.docstatus = 1
|
||||
{condition2}
|
||||
""",
|
||||
params,
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account=%(account)s
|
||||
{condition1}
|
||||
and gle.fiscal_year=%(fiscal_year)s
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""",
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_accumulated_monthly_budget(budget_name, posting_date):
|
||||
posting_date = getdate(posting_date)
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
bd = frappe.qb.DocType("Budget Distribution")
|
||||
b = frappe.qb.DocType("Budget")
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(bd)
|
||||
.join(b)
|
||||
.on(bd.parent == b.name)
|
||||
.select(Sum(bd.amount).as_("accumulated_amount"))
|
||||
.where(b.name == budget_name)
|
||||
.where(bd.start_date <= posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
return flt(result[0]["accumulated_amount"]) if result else 0.0
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution 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
|
||||
|
||||
if not params.get("company"):
|
||||
if not args.get("company"):
|
||||
return cost_center, expense_account
|
||||
|
||||
if params.item_code:
|
||||
if args.item_code:
|
||||
item_defaults = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": params.item_code, "company": params.get("company")},
|
||||
{"parent": args.item_code, "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
if item_defaults:
|
||||
@@ -809,7 +539,7 @@ def get_item_details(params):
|
||||
|
||||
if not (cost_center and expense_account):
|
||||
for doctype in ["Item Group", "Company"]:
|
||||
data = get_expense_cost_center(doctype, params)
|
||||
data = get_expense_cost_center(doctype, args)
|
||||
|
||||
if not cost_center and data:
|
||||
cost_center = data[0]
|
||||
@@ -823,39 +553,14 @@ def get_item_details(params):
|
||||
return cost_center, expense_account
|
||||
|
||||
|
||||
def get_expense_cost_center(doctype, params):
|
||||
def get_expense_cost_center(doctype, args):
|
||||
if doctype == "Item Group":
|
||||
return frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
|
||||
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_value(
|
||||
doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
)
|
||||
|
||||
|
||||
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||
from_year = frappe.get_cached_value(
|
||||
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
to_year = frappe.get_cached_value(
|
||||
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
return from_year.year_start_date, to_year.year_end_date
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def revise_budget(budget_name):
|
||||
old_budget = frappe.get_doc("Budget", budget_name)
|
||||
|
||||
if old_budget.docstatus == 1:
|
||||
old_budget.cancel()
|
||||
|
||||
new_budget = frappe.copy_doc(old_budget)
|
||||
new_budget.docstatus = 0
|
||||
new_budget.revision_of = old_budget.name
|
||||
new_budget.insert()
|
||||
|
||||
return new_budget.name
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.client import submit
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
revise_budget,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -26,16 +24,12 @@ class TestBudget(ERPNextTestSuite):
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
self.company = "_Test Company"
|
||||
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
|
||||
self.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
self.cost_center = "_Test Cost Center - _TC"
|
||||
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
|
||||
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -56,13 +50,12 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -80,11 +73,13 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_exception_approver_role(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -112,16 +107,16 @@ class TestBudget(ERPNextTestSuite):
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -156,15 +151,14 @@ class TestBudget(ERPNextTestSuite):
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
@@ -181,14 +175,13 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -207,7 +200,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -224,7 +217,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
@@ -244,7 +237,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -273,7 +266,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
budget = make_budget(budget_against="Project")
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -305,17 +298,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Company - _TC",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -344,14 +331,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
|
||||
)
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.name,
|
||||
nowdate(),
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -388,12 +372,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="Main Budget Cost Center 1 - _TC",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -408,14 +387,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_action_for_cumulative_limit(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -446,165 +422,6 @@ class TestBudget(ERPNextTestSuite):
|
||||
po.cancel()
|
||||
jv.cancel()
|
||||
|
||||
def test_fiscal_year_validation(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2100",
|
||||
"year_start_date": "2100-04-01",
|
||||
"year_end_date": "2101-03-31",
|
||||
"companies": [{"company": "_Test Company"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
from_fiscal_year="2100",
|
||||
to_fiscal_year="2099",
|
||||
do_not_save=True,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_total_distribution_equals_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for row in budget.budget_distribution:
|
||||
row.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_evenly_distribute_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
total = sum([d.amount for d in budget.budget_distribution])
|
||||
self.assertEqual(flt(total), 120000)
|
||||
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
|
||||
|
||||
def test_create_revised_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
self.assertNotEqual(budget.name, revised_budget.name)
|
||||
self.assertEqual(revised_budget.budget_against, budget.budget_against)
|
||||
self.assertEqual(revised_budget.budget_amount, budget.budget_amount)
|
||||
|
||||
old_budget = frappe.get_doc("Budget", budget.name)
|
||||
self.assertEqual(old_budget.docstatus, 2)
|
||||
|
||||
def test_revision_preserves_distribution(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
|
||||
self.assertGreater(len(revised_budget.budget_distribution), 0)
|
||||
|
||||
total = sum(row.amount for row in revised_budget.budget_distribution)
|
||||
self.assertEqual(total, revised_budget.budget_amount)
|
||||
|
||||
def test_manual_budget_amount_total(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=0,
|
||||
budget_amount=30000,
|
||||
budget_start_date="2025-04-01",
|
||||
budget_end_date="2025-06-30",
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
budget.budget_distribution = []
|
||||
|
||||
for row in [
|
||||
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
|
||||
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
|
||||
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
|
||||
]:
|
||||
budget.append("budget_distribution", row)
|
||||
|
||||
budget.save()
|
||||
|
||||
total_child_amount = sum(row.amount for row in budget.budget_distribution)
|
||||
|
||||
self.assertEqual(total_child_amount, budget.budget_amount)
|
||||
|
||||
def test_fiscal_year_company_mismatch(self):
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=True, submit_budget=False)
|
||||
|
||||
fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2099",
|
||||
"year_start_date": "2099-04-01",
|
||||
"year_end_date": "2100-03-31",
|
||||
"companies": [{"company": "_Test Company 2"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget.from_fiscal_year = fy.name
|
||||
budget.to_fiscal_year = fy.name
|
||||
budget.company = "_Test Company"
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_manual_distribution_total_equals_budget_amount(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Cost Center - _TC",
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for d in budget.budget_distribution:
|
||||
d.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_duplicate_budget_validation(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=1,
|
||||
budget_amount=15000,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
new_budget = frappe.new_doc("Budget")
|
||||
new_budget.company = "_Test Company"
|
||||
new_budget.from_fiscal_year = budget.from_fiscal_year
|
||||
new_budget.to_fiscal_year = new_budget.from_fiscal_year
|
||||
new_budget.budget_against = "Cost Center"
|
||||
new_budget.cost_center = "_Test Cost Center - _TC"
|
||||
new_budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
new_budget.budget_amount = 10000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
new_budget.insert()
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
@@ -613,32 +430,21 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
||||
budget_against = budget_against_CC or "_Test Cost Center - _TC"
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
|
||||
|
||||
args = frappe._dict(
|
||||
{
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"month_end_date": posting_date,
|
||||
"monthly_end_date": posting_date,
|
||||
"company": "_Test Company",
|
||||
"from_fiscal_year": fiscal_year,
|
||||
"to_fiscal_year": fiscal_year,
|
||||
"fiscal_year": fiscal_year,
|
||||
"budget_against_field": budget_against_field,
|
||||
"budget_start_date": fiscal_year_start_date,
|
||||
"budget_end_date": fiscal_year_end_date,
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get(budget_against_field):
|
||||
args[budget_against_field] = budget_against
|
||||
|
||||
args.budget_against_doctype = frappe.unscrub(budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
|
||||
args.is_tree = True
|
||||
else:
|
||||
args.is_tree = False
|
||||
|
||||
existing_expense = get_actual_expense(args)
|
||||
|
||||
if existing_expense:
|
||||
@@ -668,33 +474,18 @@ def make_budget(**args):
|
||||
|
||||
budget_against = args.budget_against
|
||||
cost_center = args.cost_center
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"project": project,
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
project_name = "{}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
else:
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"cost_center": cost_center or "_Test Cost Center - _TC",
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for name in budget_list:
|
||||
doc = frappe.get_doc("Budget", name)
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
frappe.delete_doc("Budget", name, force=True, ignore_missing=True)
|
||||
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
|
||||
budget = frappe.new_doc("Budget")
|
||||
|
||||
@@ -703,18 +494,18 @@ def make_budget(**args):
|
||||
else:
|
||||
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
||||
|
||||
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
|
||||
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
|
||||
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
|
||||
monthly_distribution.fiscal_year = fiscal_year
|
||||
monthly_distribution.save()
|
||||
|
||||
budget.fiscal_year = fiscal_year
|
||||
budget.monthly_distribution = "_Test Distribution"
|
||||
budget.company = "_Test Company"
|
||||
budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
budget.budget_amount = args.budget_amount or 200000
|
||||
budget.applicable_on_booking_actual_expenses = 1
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
|
||||
budget.distribution_frequency = "Monthly"
|
||||
budget.distribute_equally = args.get("distribute_equally", 1)
|
||||
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
@@ -739,13 +530,7 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
budget.insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
if args.submit_budget:
|
||||
budget.submit()
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
return budget
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-12 23:31:03.841996",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"amount",
|
||||
"percent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "percent",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percent"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 13:18:28.398198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Distribution",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -101,11 +101,10 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:02.873815",
|
||||
"modified": "2024-03-27 13:06:47.653110",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -142,9 +141,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,13 +105,11 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
currency=company_currency,
|
||||
company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
@@ -83,11 +81,10 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 8500.0)
|
||||
self.assertEqual(je.total_credit, 8500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance")],
|
||||
fields=["sum(debit)-sum(credit) as balance"],
|
||||
)[0]
|
||||
self.assertEqual(acc_balance.balance, 8500.0)
|
||||
|
||||
@@ -149,15 +146,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 500.0)
|
||||
self.assertEqual(je.total_credit, 500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency
|
||||
@@ -199,15 +193,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account should have balance only in account currency
|
||||
@@ -244,15 +235,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency post revaluation
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-09-06 09:39:46.503678",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_code",
|
||||
"display_name",
|
||||
"indentation_level",
|
||||
"data_source",
|
||||
"balance_type",
|
||||
"column_break_hxqu",
|
||||
"fieldtype",
|
||||
"color",
|
||||
"bold_text",
|
||||
"italic_text",
|
||||
"hidden_calculation",
|
||||
"hide_when_empty",
|
||||
"reverse_sign",
|
||||
"include_in_charts",
|
||||
"section_break_ornw",
|
||||
"column_break_asfe",
|
||||
"advanced_filtering",
|
||||
"filters_editor",
|
||||
"calculation_formula",
|
||||
"section_break_pvro",
|
||||
"formula_description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "Code to reference this line in formulas (e.g., REV100, EXP200, ASSET100)",
|
||||
"fieldname": "reference_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Line Reference"
|
||||
},
|
||||
{
|
||||
"description": "Text displayed on the financial statement (e.g., 'Total Revenue', 'Cash and Cash Equivalents')",
|
||||
"fieldname": "display_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Display Name"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "Indentation level: 0 = Main heading, 1 = Sub-category, 2 = Individual accounts, etc.",
|
||||
"fieldname": "indentation_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Indent Level"
|
||||
},
|
||||
{
|
||||
"description": "How this line gets its data",
|
||||
"fieldname": "data_source",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Data Source",
|
||||
"options": "\nAccount Data\nCalculated Amount\nCustom API\nBlank Line\nColumn Break\nSection Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.data_source == 'Account Data'",
|
||||
"description": "Opening Balance = Start of period, Closing Balance = End of period, Period Movement = Net change during period",
|
||||
"fieldname": "balance_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Balance Type",
|
||||
"mandatory_depends_on": "eval:doc.data_source == 'Account Data'",
|
||||
"options": "\nOpening Balance\nClosing Balance\nPeriod Movement (Debits - Credits)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hxqu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Bold text for emphasis (totals, major headings)",
|
||||
"fieldname": "bold_text",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Italic text for subtotals or notes",
|
||||
"fieldname": "italic_text",
|
||||
"fieldtype": "Check",
|
||||
"label": "Italic Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Calculate but don't show on final report",
|
||||
"fieldname": "hidden_calculation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden Line (Internal Use Only)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Hide this line if amount is zero",
|
||||
"fieldname": "hide_when_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide If Zero"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "0",
|
||||
"description": "Show negative values as positive (for expenses in P&L)",
|
||||
"fieldname": "reverse_sign",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reverse Sign"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ornw",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.data_source === \"Account Data\" && doc.advanced_filtering) || [\"Calculated Amount\", \"Custom API\"].includes(doc.data_source);\n",
|
||||
"fieldname": "calculation_formula",
|
||||
"fieldtype": "Code",
|
||||
"label": "Formula or Account Filter",
|
||||
"mandatory_depends_on": "eval:doc.data_source != 'Blank Line' && doc.data_source != 'Column Break' && doc.data_source != 'Section Break'"
|
||||
},
|
||||
{
|
||||
"fieldname": "formula_description",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, this row's values will be displayed on financial charts",
|
||||
"fieldname": "include_in_charts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include in Charts"
|
||||
},
|
||||
{
|
||||
"description": "Color to highlight values (e.g., red for exceptions)",
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"label": "Color"
|
||||
},
|
||||
{
|
||||
"description": "How to format and present values in the financial report (only if different from column fieldtype)",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"label": "Value Type",
|
||||
"options": "\nCurrency\nFloat\nInt\nPercent"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.data_source === \"Account Data\" && !doc.advanced_filtering",
|
||||
"fieldname": "filters_editor",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: ![\"Blank Line\", \"Column Break\", \"Section Break\"].includes(doc.data_source);",
|
||||
"fieldname": "column_break_asfe",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.data_source === \"Account Data\"",
|
||||
"description": "Use <strong>Python</strong> filters to get Accounts",
|
||||
"fieldname": "advanced_filtering",
|
||||
"fieldtype": "Check",
|
||||
"label": "Advanced Filtering",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pvro",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 09:23:27.208072",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Row",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class FinancialReportRow(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
|
||||
|
||||
advanced_filtering: DF.Check
|
||||
balance_type: DF.Literal[
|
||||
"", "Opening Balance", "Closing Balance", "Period Movement (Debits - Credits)"
|
||||
]
|
||||
bold_text: DF.Check
|
||||
calculation_formula: DF.Code | None
|
||||
color: DF.Color | None
|
||||
data_source: DF.Literal[
|
||||
"",
|
||||
"Account Data",
|
||||
"Calculated Amount",
|
||||
"Custom API",
|
||||
"Blank Line",
|
||||
"Column Break",
|
||||
"Section Break",
|
||||
]
|
||||
display_name: DF.Data | None
|
||||
fieldtype: DF.Literal["", "Currency", "Float", "Int", "Percent"]
|
||||
hidden_calculation: DF.Check
|
||||
hide_when_empty: DF.Check
|
||||
include_in_charts: DF.Check
|
||||
indentation_level: DF.Int
|
||||
italic_text: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_code: DF.Data | None
|
||||
reverse_sign: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,433 +0,0 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Financial Report Template", {
|
||||
refresh(frm) {
|
||||
// add custom button to view missed accounts
|
||||
frm.add_custom_button(__("View Account Coverage"), function () {
|
||||
let selected_rows = frm.get_field("rows").grid.get_selected_children();
|
||||
const has_selection = selected_rows.length > 0;
|
||||
if (selected_rows.length === 0) selected_rows = frm.doc.rows;
|
||||
|
||||
show_accounts_tree(selected_rows, has_selection);
|
||||
});
|
||||
|
||||
// add custom button to open the financial report
|
||||
frm.add_custom_button(__("View Report"), function () {
|
||||
frappe.set_route("query-report", frm.doc.report_type, {
|
||||
report_template: frm.doc.name,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
validate(frm) {
|
||||
if (!frm.doc.rows || frm.doc.rows.length === 0) {
|
||||
frappe.msgprint(__("At least one row is required for a financial report template"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Financial Report Row", {
|
||||
data_source(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
update_formula_label(frm, row.data_source);
|
||||
update_formula_description(frm, row.data_source);
|
||||
|
||||
if (row.data_source !== "Account Data") {
|
||||
frappe.model.set_value(cdt, cdn, "balance_type", "");
|
||||
}
|
||||
|
||||
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
|
||||
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
|
||||
}
|
||||
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
form_render(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
update_formula_label(frm, row.data_source);
|
||||
update_advanced_formula_property(frm, cdt, cdn);
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
update_formula_description(frm, row.data_source);
|
||||
},
|
||||
|
||||
calculation_formula(frm, cdt, cdn) {
|
||||
update_advanced_formula_property(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
advanced_filtering(frm, cdt, cdn) {
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
},
|
||||
});
|
||||
|
||||
// FILTERS EDITOR
|
||||
|
||||
function set_up_filters_editor(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
if (row.data_source !== "Account Data" || row.advanced_filtering) return;
|
||||
|
||||
const grid_row = frm.fields_dict["rows"].grid.get_row(cdn);
|
||||
const wrapper = grid_row.get_field("filters_editor").$wrapper;
|
||||
wrapper.empty();
|
||||
|
||||
const ACCOUNT = "Account";
|
||||
const FIELD_IDX = 1;
|
||||
const OPERATOR_IDX = 2;
|
||||
const VALUE_IDX = 3;
|
||||
|
||||
// Parse saved filters
|
||||
let saved_filters = [];
|
||||
|
||||
if (row.calculation_formula) {
|
||||
try {
|
||||
const parsed = JSON.parse(row.calculation_formula);
|
||||
|
||||
if (Array.isArray(parsed)) saved_filters = [parsed];
|
||||
else if (parsed.and) saved_filters = parsed.and;
|
||||
} catch (e) {
|
||||
frappe.show_alert({
|
||||
message: __("Invalid filter formula. Please check the syntax."),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (saved_filters.length)
|
||||
// Ensure every filter starts with "Account"
|
||||
saved_filters = saved_filters.map((f) => [ACCOUNT, ...f]);
|
||||
|
||||
frappe.model.with_doctype(ACCOUNT, () => {
|
||||
const filter_group = new frappe.ui.FilterGroup({
|
||||
parent: wrapper,
|
||||
doctype: ACCOUNT,
|
||||
on_change: () => {
|
||||
// only need [[field, operator, value]]
|
||||
const filters = filter_group
|
||||
.get_filters()
|
||||
.map((f) => [f[FIELD_IDX], f[OPERATOR_IDX], f[VALUE_IDX]]);
|
||||
|
||||
const current = filters.length > 1 ? { and: filters } : filters[0];
|
||||
frappe.model.set_value(cdt, cdn, "calculation_formula", JSON.stringify(current));
|
||||
},
|
||||
});
|
||||
|
||||
filter_group.add_filters_to_filter_group(saved_filters);
|
||||
});
|
||||
}
|
||||
|
||||
function update_advanced_formula_property(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
const is_advanced = is_advanced_formula(row);
|
||||
|
||||
frm.set_df_property("rows", "read_only", is_advanced, frm.doc.name, "advanced_filtering", cdn);
|
||||
|
||||
if (is_advanced && !row.advanced_filtering) {
|
||||
row.advanced_filtering = 1;
|
||||
frm.refresh_field("rows");
|
||||
}
|
||||
}
|
||||
|
||||
function is_advanced_formula(row) {
|
||||
if (!row || row.data_source !== "Account Data") return false;
|
||||
|
||||
let parsed = null;
|
||||
if (row.calculation_formula) {
|
||||
try {
|
||||
parsed = JSON.parse(row.calculation_formula);
|
||||
} catch (e) {
|
||||
console.warn("Invalid JSON in calculation_formula:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) return false;
|
||||
if (parsed?.or) return true;
|
||||
if (parsed?.and) return parsed.and.some((cond) => !Array.isArray(cond));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ACCOUNTS TREE VIEW
|
||||
|
||||
function show_accounts_tree(template_rows, has_selection) {
|
||||
// filtered rows
|
||||
const account_rows = template_rows.filter((row) => row.data_source === "Account Data");
|
||||
|
||||
if (account_rows.length === 0) {
|
||||
frappe.show_alert(__("No <strong>Account Data</strong> row found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Accounts Missing from Report"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
label: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
onchange: () => {
|
||||
const company_field = dialog.get_field("company");
|
||||
if (!company_field.value || company_field.value === company_field.last_value) return;
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "view_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Missing Accounts", "Filtered Accounts"],
|
||||
label: "View",
|
||||
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
dialog.set_title(
|
||||
dialog.get_value("view_type") === "Missing Accounts"
|
||||
? __("Accounts Missing from Report")
|
||||
: __("Accounts Included in Report")
|
||||
);
|
||||
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "tip",
|
||||
fieldtype: "HTML",
|
||||
label: "Tip",
|
||||
options: `
|
||||
<div class="alert alert-success" role="alert">
|
||||
Tip: Select report lines to view their accounts
|
||||
</div>
|
||||
`,
|
||||
depends_on: has_selection ? "eval: false" : "eval: true",
|
||||
},
|
||||
{
|
||||
fieldname: "tree_area",
|
||||
fieldtype: "HTML",
|
||||
label: "Chart of Accounts",
|
||||
read_only: 1,
|
||||
depends_on: "eval: doc.company",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Done"),
|
||||
primary_action() {
|
||||
dialog.hide();
|
||||
},
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
}
|
||||
|
||||
async function refresh_tree_view(dialog, account_rows) {
|
||||
const missed = dialog.get_value("view_type") === "Missing Accounts";
|
||||
const company = dialog.get_value("company");
|
||||
|
||||
const wrapper = dialog.get_field("tree_area").$wrapper;
|
||||
wrapper.empty();
|
||||
|
||||
// get filtered accounts
|
||||
const { message: filtered_accounts } = await frappe.call({
|
||||
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_filtered_accounts",
|
||||
args: { company: company, account_rows: account_rows },
|
||||
});
|
||||
|
||||
// render tree
|
||||
const tree = new FilteredTree({
|
||||
parent: wrapper,
|
||||
label: company,
|
||||
root_value: company,
|
||||
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
|
||||
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
|
||||
toolbar: [],
|
||||
});
|
||||
|
||||
tree.load_children(tree.root_node, true);
|
||||
}
|
||||
|
||||
class FilteredTree extends frappe.ui.Tree {
|
||||
render_children_of_all_nodes(data_list) {
|
||||
data_list = this.get_filtered_data_list(data_list);
|
||||
super.render_children_of_all_nodes(data_list);
|
||||
}
|
||||
|
||||
get_filtered_data_list(data_list) {
|
||||
let removed_nodes = new Set();
|
||||
|
||||
// Filter nodes with no data
|
||||
data_list = data_list.filter((d) => {
|
||||
if (d.data.length === 0) {
|
||||
removed_nodes.add(d.parent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove references to removed nodes and iteratively remove empty parents
|
||||
while (removed_nodes.size > 0) {
|
||||
const current_removed = [...removed_nodes];
|
||||
removed_nodes.clear();
|
||||
|
||||
data_list = data_list.filter((d) => {
|
||||
d.data = d.data.filter((a) => !current_removed.includes(a.value));
|
||||
|
||||
if (d.data.length === 0) {
|
||||
removed_nodes.add(d.parent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return data_list;
|
||||
}
|
||||
}
|
||||
|
||||
function update_formula_label(frm, data_source) {
|
||||
const grid = frm.fields_dict.rows.grid;
|
||||
const field = grid.fields_map.calculation_formula;
|
||||
if (!field) return;
|
||||
|
||||
const labels = {
|
||||
"Account Data": "Account Filter",
|
||||
"Custom API": "API Method Path",
|
||||
};
|
||||
|
||||
grid.update_docfield_property(
|
||||
"calculation_formula",
|
||||
"label",
|
||||
labels[data_source] || "Calculation Formula"
|
||||
);
|
||||
}
|
||||
|
||||
// FORMULA DESCRIPTION
|
||||
|
||||
function update_formula_description(frm, data_source) {
|
||||
if (!data_source) return;
|
||||
|
||||
let grid = frm.fields_dict.rows.grid;
|
||||
let field = grid.fields_map.formula_description;
|
||||
if (!field) return;
|
||||
|
||||
// Common CSS styles and elements
|
||||
const container_style = `style="padding: var(--padding-md); border: 1px solid var(--border-color); border-radius: var(--border-radius); margin-top: var(--margin-sm);"`;
|
||||
const title_style = `style="margin-top: 0; color: var(--text-color);"`;
|
||||
const subtitle_style = `style="color: var(--text-color); margin-bottom: var(--margin-xs);"`;
|
||||
const text_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted);"`;
|
||||
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
|
||||
|
||||
let description_html = "";
|
||||
|
||||
if (data_source === "Account Data") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Account Filter Guide</h5>
|
||||
<p ${text_style}>Specify which accounts to include in this line.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Basic Examples:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>["account_type", "=", "Cash"]</code> - All Cash accounts</li>
|
||||
<li><code>["root_type", "in", ["Asset", "Liability"]]</code> - All Asset and Liability accounts</li>
|
||||
<li><code>["account_category", "like", "Revenue"]</code> - Revenue accounts</li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Multiple Conditions (AND/OR):</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>{"and": [["root_type", "=", "Asset"], ["account_type", "=", "Cash"]]}</code></li>
|
||||
<li><code>{"or": [["account_category", "like", "Revenue"], ["account_category", "like", "Income"]]}</code></li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Available operators:</strong> <code>=, !=, in, not in, like, not like, is</code></p>
|
||||
<p ${tip_style}><strong>Multi-Company Tip:</strong> Use fields like <code>account_type</code>, <code>root_type</code>, and <code>account_category</code> for templates that work across multiple companies.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Calculated Amount") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Formula Guide</h5>
|
||||
<p ${text_style}>Create calculations using reference codes from other lines.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Basic Examples:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>REV100 + REV200</code> - Add two revenue lines</li>
|
||||
<li><code>ASSETS - LIABILITIES</code> - Calculate equity</li>
|
||||
<li><code>REVENUE * 0.1</code> - 10% of revenue</li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Common Functions:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>abs(value)</code> - Remove negative sign</li>
|
||||
<li><code>round(value)</code> - Round to whole number</li>
|
||||
<li><code>max(val1, val2)</code> - Larger of two values</li>
|
||||
<li><code>min(val1, val2)</code> - Smaller of two values</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Required:</strong> Use "Reference Code" from other rows in your formulas.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Custom API") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Custom API Setup</h5>
|
||||
<p ${text_style}>Path to your custom method that returns financial data.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Format:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>erpnext.custom.financial_apis.get_custom_revenue</code></li>
|
||||
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Return Format:</h6>
|
||||
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
||||
</div>`;
|
||||
} else if (data_source === "Blank Line") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Blank Line</h5>
|
||||
<p ${text_style}>Adds empty space for better visual separation.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Separating major sections</li>
|
||||
<li>Adding space before totals</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - creates visual spacing only.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Column Break") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Column Break</h5>
|
||||
<p ${text_style}>Creates a visual break for side-by-side layout.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Horizontal P&L statements</li>
|
||||
<li>Side-by-side Balance Sheet sections</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Section Break") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Section Break</h5>
|
||||
<p ${text_style}>Creates a visual break for separating different sections.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Separating major sections in a report - say trading & profit and loss</li>
|
||||
<li>Improving readability by adding space</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
grid.update_docfield_property("formula_description", "options", description_html);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2025-08-02 04:44:15.184541",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"template_name",
|
||||
"report_type",
|
||||
"module",
|
||||
"column_break_lvnq",
|
||||
"disabled",
|
||||
"section_break_fvlw",
|
||||
"rows"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Descriptive name for your template (e.g., 'Standard P&L', 'Detailed Balance Sheet')",
|
||||
"fieldname": "template_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Template Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"description": "Type of financial statement this template generates",
|
||||
"fieldname": "report_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:frappe.boot.developer_mode",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for Export)",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lvnq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fvlw",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "rows",
|
||||
"fieldtype": "Table",
|
||||
"label": "Report Line Items",
|
||||
"options": "Financial Report Row"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Disable template to prevent use in reports",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-14 00:11:03.508139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Template",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "template_name"
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.account_category.account_category import import_account_categories
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import TemplateValidator
|
||||
|
||||
|
||||
class FinancialReportTemplate(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
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_row.financial_report_row import FinancialReportRow
|
||||
|
||||
disabled: DF.Check
|
||||
module: DF.Link | None
|
||||
report_type: DF.Literal[
|
||||
"", "Profit and Loss Statement", "Balance Sheet", "Cash Flow", "Custom Financial Statement"
|
||||
]
|
||||
rows: DF.Table[FinancialReportRow]
|
||||
template_name: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
validator = TemplateValidator(self)
|
||||
result = validator.validate()
|
||||
result.notify_user()
|
||||
|
||||
def on_update(self):
|
||||
self._export_template()
|
||||
|
||||
def on_trash(self):
|
||||
self._delete_template()
|
||||
|
||||
def _export_template(self):
|
||||
from frappe.modules.utils import export_module_json
|
||||
|
||||
if not self.module:
|
||||
return
|
||||
|
||||
export_module_json(self, True, self.module)
|
||||
self._export_account_categories()
|
||||
|
||||
def _delete_template(self):
|
||||
if not self.module or not frappe.conf.developer_mode:
|
||||
return
|
||||
|
||||
module_path = frappe.get_module_path(self.module)
|
||||
dir_path = os.path.join(module_path, "financial_report_template", frappe.scrub(self.name))
|
||||
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
|
||||
def _export_account_categories(self):
|
||||
import json
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FormulaFieldExtractor,
|
||||
)
|
||||
|
||||
if not self.module or not frappe.conf.developer_mode or frappe.flags.in_import:
|
||||
return
|
||||
|
||||
# Extract category from rows
|
||||
extractor = FormulaFieldExtractor(
|
||||
field_name="account_category", exclude_operators=["like", "not like"]
|
||||
)
|
||||
account_data_rows = [row for row in self.rows if row.data_source == "Account Data"]
|
||||
category_names = extractor.extract_from_rows(account_data_rows)
|
||||
|
||||
if not category_names:
|
||||
return
|
||||
|
||||
# Get path
|
||||
module_path = frappe.get_module_path(self.module)
|
||||
categories_file = os.path.join(module_path, "financial_report_template", "account_categories.json")
|
||||
|
||||
# Load existing categories
|
||||
existing_categories = {}
|
||||
if os.path.exists(categories_file):
|
||||
try:
|
||||
with open(categories_file) as f:
|
||||
existing_data = json.load(f)
|
||||
existing_categories = {cat["account_category_name"]: cat for cat in existing_data}
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass # Create new file
|
||||
|
||||
# Fetch categories from database
|
||||
if category_names:
|
||||
db_categories = frappe.get_all(
|
||||
"Account Category",
|
||||
filters={"account_category_name": ["in", list(category_names)]},
|
||||
fields=["account_category_name", "description"],
|
||||
)
|
||||
|
||||
for cat in db_categories:
|
||||
existing_categories[cat["account_category_name"]] = cat
|
||||
|
||||
# Sort by category name
|
||||
sorted_categories = sorted(existing_categories.values(), key=lambda x: x["account_category_name"])
|
||||
|
||||
# Write to file
|
||||
os.makedirs(os.path.dirname(categories_file), exist_ok=True)
|
||||
with open(categories_file, "w") as f:
|
||||
json.dump(sorted_categories, f, indent=2)
|
||||
|
||||
|
||||
def sync_financial_report_templates(chart_of_accounts=None, existing_company=None):
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import get_chart
|
||||
|
||||
# If COA is being created for an existing company,
|
||||
# skip syncing templates as they are likely already present
|
||||
if existing_company:
|
||||
return
|
||||
|
||||
# Allow regional templates to completely override ERPNext
|
||||
# templates based on the chart of accounts selected
|
||||
disable_default_financial_report_template = False
|
||||
if chart_of_accounts:
|
||||
coa = get_chart(chart_of_accounts)
|
||||
if coa.get("disable_default_financial_report_template", False):
|
||||
disable_default_financial_report_template = True
|
||||
|
||||
installed_apps = frappe.get_installed_apps()
|
||||
|
||||
for app in installed_apps:
|
||||
if disable_default_financial_report_template and app == "erpnext":
|
||||
continue
|
||||
|
||||
_sync_templates_for(app)
|
||||
|
||||
|
||||
def _sync_templates_for(app_name):
|
||||
templates = []
|
||||
|
||||
for module_name in frappe.local.app_modules.get(app_name) or []:
|
||||
module_path = frappe.get_module_path(module_name)
|
||||
template_path = os.path.join(module_path, "financial_report_template")
|
||||
|
||||
if not os.path.isdir(template_path):
|
||||
continue
|
||||
|
||||
import_account_categories(template_path)
|
||||
|
||||
for template_dir in os.listdir(template_path):
|
||||
json_file = os.path.join(template_path, template_dir, f"{template_dir}.json")
|
||||
if os.path.isfile(json_file):
|
||||
templates.append(json_file)
|
||||
|
||||
if not templates:
|
||||
return
|
||||
|
||||
# ensure files are not exported
|
||||
frappe.flags.in_import = True
|
||||
|
||||
for template_path in templates:
|
||||
with open(template_path) as f:
|
||||
template_data = frappe._dict(frappe.parse_json(f.read()))
|
||||
|
||||
template_name = template_data.get("name")
|
||||
|
||||
if not frappe.db.exists("Financial Report Template", template_name):
|
||||
doc = frappe.get_doc(template_data)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert()
|
||||
|
||||
frappe.flags.in_import = False
|
||||
@@ -1,545 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.database.query import SQLFunctionParser
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""Represents a single validation issue"""
|
||||
|
||||
message: str
|
||||
row_idx: int | None = None
|
||||
field: str | None = None
|
||||
details: dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.details is None:
|
||||
self.details = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
|
||||
field_info = f"[{self.field}] " if self.field else ""
|
||||
message = f"{prefix}{field_info}{self.message}"
|
||||
return _(message)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
issues: list[ValidationIssue] = field(default_factory=list)
|
||||
warnings: list[ValidationIssue] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return len(self.issues) == 0
|
||||
|
||||
@property
|
||||
def has_warnings(self) -> bool:
|
||||
return len(self.warnings) > 0
|
||||
|
||||
@property
|
||||
def error_count(self) -> int:
|
||||
return len(self.issues)
|
||||
|
||||
@property
|
||||
def warning_count(self) -> int:
|
||||
return len(self.warnings)
|
||||
|
||||
def merge(self, other: "ValidationResult") -> "ValidationResult":
|
||||
self.issues.extend(other.issues)
|
||||
self.warnings.extend(other.warnings)
|
||||
return self
|
||||
|
||||
def add_error(self, issue: ValidationIssue) -> None:
|
||||
"""Add a critical error that prevents functionality"""
|
||||
self.issues.append(issue)
|
||||
|
||||
def add_warning(self, issue: ValidationIssue) -> None:
|
||||
"""Add a warning for recommendatory validation"""
|
||||
self.warnings.append(issue)
|
||||
|
||||
def notify_user(self) -> None:
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues)
|
||||
|
||||
if warnings:
|
||||
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
||||
|
||||
if errors:
|
||||
frappe.throw(errors, title=_("Errors"))
|
||||
|
||||
|
||||
class TemplateValidator:
|
||||
"""Main validator that orchestrates all validations"""
|
||||
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.validators = [
|
||||
TemplateStructureValidator(),
|
||||
DependencyValidator(template),
|
||||
]
|
||||
self.formula_validator = FormulaValidator(template)
|
||||
|
||||
def validate(self) -> ValidationResult:
|
||||
result = ValidationResult([])
|
||||
|
||||
# Run template-level validators
|
||||
for validator in self.validators:
|
||||
result.merge(validator.validate(self.template))
|
||||
|
||||
# Run row-level validations
|
||||
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
for row in self.template.rows:
|
||||
result.merge(self.formula_validator.validate(row, account_fields))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Validator(ABC):
|
||||
@abstractmethod
|
||||
def validate(self, context: Any) -> ValidationResult:
|
||||
pass
|
||||
|
||||
|
||||
class TemplateStructureValidator(Validator):
|
||||
def validate(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
result.merge(self._validate_reference_codes(template))
|
||||
result.merge(self._validate_required_fields(template))
|
||||
|
||||
return result
|
||||
|
||||
def _validate_reference_codes(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
used_codes = set()
|
||||
|
||||
for row in template.rows:
|
||||
if not row.reference_code:
|
||||
continue
|
||||
|
||||
ref_code = row.reference_code.strip()
|
||||
|
||||
# Check format
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Check uniqueness
|
||||
if ref_code in used_codes:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Duplicate line reference: '{ref_code}'",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
used_codes.add(ref_code)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_required_fields(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
for row in template.rows:
|
||||
# Balance type required
|
||||
if row.data_source == "Account Data" and not row.balance_type:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Balance Type is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Calculation formula required
|
||||
if row.data_source in ["Account Data", "Calculated Amount", "Custom API"]:
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula is required for {row.data_source}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DependencyValidator(Validator):
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.dependencies = self._build_dependency_graph()
|
||||
|
||||
def validate(self, context=None) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
result.merge(self._validate_circular_dependencies())
|
||||
result.merge(self._validate_missing_dependencies())
|
||||
|
||||
return result
|
||||
|
||||
def _build_dependency_graph(self) -> dict[str, list[str]]:
|
||||
graph = {}
|
||||
available_codes = {row.reference_code for row in self.template.rows if row.reference_code}
|
||||
|
||||
for row in self.template.rows:
|
||||
if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
|
||||
deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
|
||||
if deps:
|
||||
graph[row.reference_code] = deps
|
||||
|
||||
return graph
|
||||
|
||||
def _validate_circular_dependencies(self) -> ValidationResult:
|
||||
"""
|
||||
Efficient cycle detection using DFS (Depth-First Search) with three-color algorithm:
|
||||
- WHITE (0): unvisited node
|
||||
- GRAY (1): currently being processed (on recursion stack)
|
||||
- BLACK (2): fully processed
|
||||
|
||||
Example cycle detection:
|
||||
A → B → C → A (cycle detected when A is GRAY and visited again)
|
||||
"""
|
||||
result = ValidationResult()
|
||||
WHITE, GRAY, BLACK = 0, 1, 2
|
||||
colors = {node: WHITE for node in self.dependencies}
|
||||
|
||||
def dfs(node, path):
|
||||
if node not in colors:
|
||||
return # External dependency
|
||||
|
||||
if colors[node] == GRAY:
|
||||
# Found cycle
|
||||
cycle_start = path.index(node)
|
||||
cycle = [*path[cycle_start:], node]
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Circular dependency detected: {' → '.join(cycle)}",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if colors[node] == BLACK:
|
||||
return # Already processed
|
||||
|
||||
colors[node] = GRAY
|
||||
path.append(node)
|
||||
|
||||
for neighbor in self.dependencies.get(node, []):
|
||||
dfs(neighbor, path.copy())
|
||||
|
||||
colors[node] = BLACK
|
||||
|
||||
for node in self.dependencies:
|
||||
if colors[node] == WHITE:
|
||||
dfs(node, [])
|
||||
|
||||
return result
|
||||
|
||||
def _validate_missing_dependencies(self) -> ValidationResult:
|
||||
available = {row.reference_code for row in self.template.rows if row.reference_code}
|
||||
result = ValidationResult()
|
||||
|
||||
for ref_code, deps in self.dependencies.items():
|
||||
undefined = [d for d in deps if d not in available]
|
||||
if undefined:
|
||||
row_idx = self._get_row_idx(ref_code)
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Line References undefined in Formula: {', '.join(undefined)}",
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _get_row_idx(self, reference_code: str) -> int | None:
|
||||
for row in self.template.rows:
|
||||
if row.reference_code == reference_code:
|
||||
return row.idx
|
||||
return None
|
||||
|
||||
|
||||
class CalculationFormulaValidator(Validator):
|
||||
"""Validates calculation formulas used in Calculated Amount rows"""
|
||||
|
||||
def __init__(self, reference_codes: set[str]):
|
||||
self.reference_codes = reference_codes
|
||||
|
||||
def validate(self, row) -> ValidationResult:
|
||||
"""Validate calculation formula for a single row"""
|
||||
result = ValidationResult()
|
||||
|
||||
if row.data_source != "Calculated Amount":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula is required for Calculated Amount",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
formula = self._preprocess_formula(row.calculation_formula)
|
||||
row.calculation_formula = formula
|
||||
|
||||
# Check parentheses
|
||||
if not self._are_parentheses_balanced(formula):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula has unbalanced parentheses",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Check self-reference
|
||||
available_codes = list(self.reference_codes)
|
||||
refs = extract_reference_codes_from_formula(formula, available_codes)
|
||||
if row.reference_code and row.reference_code in refs:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references itself ('{row.reference_code}')",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Check undefined references
|
||||
undefined = set(refs) - set(available_codes)
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references undefined codes: {', '.join(undefined)}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Try to evaluate with dummy values
|
||||
eval_error = self._test_formula_evaluation(formula, available_codes)
|
||||
if eval_error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula evaluation error: {eval_error}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _preprocess_formula(self, formula: str) -> str:
|
||||
if not formula or not isinstance(formula, str):
|
||||
return ""
|
||||
|
||||
return formula.strip()
|
||||
|
||||
@staticmethod
|
||||
def _are_parentheses_balanced(formula: str) -> bool:
|
||||
return formula.count("(") == formula.count(")")
|
||||
|
||||
def _test_formula_evaluation(self, formula: str, available_codes: list[str]) -> str | None:
|
||||
try:
|
||||
context = {code: 1.0 for code in available_codes}
|
||||
context.update(
|
||||
{
|
||||
"abs": abs,
|
||||
"round": round,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"sum": sum,
|
||||
"sqrt": lambda x: x**0.5,
|
||||
"pow": pow,
|
||||
"ceil": lambda x: int(x) + (1 if x % 1 else 0),
|
||||
"floor": lambda x: int(x),
|
||||
}
|
||||
)
|
||||
|
||||
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
|
||||
|
||||
if not isinstance(result, (int, float)): # noqa: UP038
|
||||
return f"Formula must return a numeric value, got {type(result).__name__}"
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
class AccountFilterValidator(Validator):
|
||||
"""Validates account filter expressions used in Account Data rows"""
|
||||
|
||||
def __init__(self, account_fields: set | None = None):
|
||||
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
|
||||
|
||||
def validate(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if row.data_source != "Account Data":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Account filter is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
filter_config = json.loads(row.calculation_formula)
|
||||
error = self._validate_filter_structure(filter_config, self.account_fields)
|
||||
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=error,
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid JSON format: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
return "Filter must be [field, operator, value]"
|
||||
|
||||
field, operator, value = filter_config
|
||||
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return "Field and operator must be strings"
|
||||
|
||||
if field not in account_fields:
|
||||
return f"Field '{field}' is not a valid account field"
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return f"Invalid operator '{operator}'"
|
||||
|
||||
if operator in ["in", "not in"] and not isinstance(value, list):
|
||||
return f"Operator '{operator}' requires a list value"
|
||||
|
||||
# logical condition: {"and": [condition1, condition2]}
|
||||
elif isinstance(filter_config, dict):
|
||||
if len(filter_config) != 1:
|
||||
return "Logical condition must have exactly one operator"
|
||||
|
||||
op = next(iter(filter_config.keys())).lower()
|
||||
if op not in ["and", "or"]:
|
||||
return "Logical operators must be 'and' or 'or'"
|
||||
|
||||
conditions = filter_config[next(iter(filter_config.keys()))]
|
||||
if not isinstance(conditions, list) or len(conditions) < 1:
|
||||
return "Logical conditions need at least 1 sub-condition"
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
error = self._validate_filter_structure(condition, account_fields)
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
return "Filter must be a list or dict"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class FormulaValidator(Validator):
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
reference_codes = {row.reference_code for row in template.rows if row.reference_code}
|
||||
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
||||
self.account_filter_validator = AccountFilterValidator()
|
||||
|
||||
def validate(self, row, account_fields: set) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if not row.calculation_formula:
|
||||
return result
|
||||
|
||||
if row.data_source == "Calculated Amount":
|
||||
return self.calculation_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Account Data":
|
||||
# Update account fields if provided
|
||||
if account_fields:
|
||||
self.account_filter_validator.account_fields = account_fields
|
||||
return self.account_filter_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Custom API":
|
||||
result.merge(self._validate_custom_api(row))
|
||||
|
||||
return result
|
||||
|
||||
def _validate_custom_api(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
api_path = row.calculation_formula
|
||||
|
||||
if "." not in api_path:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Custom API path should be in format: app.module.method",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Method exists?
|
||||
try:
|
||||
module_path, method_name = api_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
|
||||
if not hasattr(module, method_name):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Could not validate API path: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_reference_codes_from_formula(formula: str, available_codes: list[str]) -> list[str]:
|
||||
found_codes = []
|
||||
for code in available_codes:
|
||||
# Match complete words only to avoid partial matches
|
||||
pattern = r"\b" + re.escape(code) + r"\b"
|
||||
if re.search(pattern, formula):
|
||||
found_codes.append(code)
|
||||
return found_codes
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests.utils import make_test_records
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class TestFinancialReportTemplate(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class FinancialReportTemplateTestCase(IntegrationTestCase):
|
||||
"""Utility class with common setup and helper methods for all test classes"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up test data"""
|
||||
make_test_records("Company")
|
||||
make_test_records("Fiscal Year")
|
||||
cls.create_test_template()
|
||||
|
||||
@classmethod
|
||||
def create_test_template(cls):
|
||||
"""Create a test financial report template"""
|
||||
if not frappe.db.exists("Financial Report Template", "Test P&L Template"):
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Financial Report Template",
|
||||
"template_name": "Test P&L Template",
|
||||
"report_type": "Profit and Loss Statement",
|
||||
"rows": [
|
||||
{
|
||||
"reference_code": "INC001",
|
||||
"display_name": "Income",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": '["root_type", "=", "Income"]',
|
||||
},
|
||||
{
|
||||
"reference_code": "EXP001",
|
||||
"display_name": "Expenses",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": '["root_type", "=", "Expense"]',
|
||||
},
|
||||
{
|
||||
"reference_code": "NET001",
|
||||
"display_name": "Net Profit/Loss",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Calculated Amount",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": "INC001 - EXP001",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
cls.test_template = frappe.get_doc("Financial Report Template", "Test P&L Template")
|
||||
|
||||
@staticmethod
|
||||
def create_test_template_with_rows(rows_data):
|
||||
"""Helper method to create test template with specific rows"""
|
||||
template_name = f"Test Template {frappe.generate_hash()[:8]}"
|
||||
template = frappe.get_doc(
|
||||
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
|
||||
)
|
||||
return template
|
||||
@@ -99,7 +99,7 @@ class FiscalYear(Document):
|
||||
)
|
||||
|
||||
overlap = False
|
||||
if not self.get("companies") and not company_for_existing:
|
||||
if not self.get("companies") or not company_for_existing:
|
||||
overlap = True
|
||||
|
||||
for d in self.get("companies"):
|
||||
|
||||
@@ -4,7 +4,6 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "fiscal_year",
|
||||
"non_standard_fieldnames": {"Budget": "from_fiscal_year"},
|
||||
"transactions": [
|
||||
{"label": _("Budgets"), "items": ["Budget"]},
|
||||
{"label": _("References"), "items": ["Period Closing Voucher"]},
|
||||
|
||||
@@ -25,27 +25,6 @@ class TestFiscalYear(IntegrationTestCase):
|
||||
|
||||
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
|
||||
|
||||
def test_company_fiscal_year_overlap(self):
|
||||
for name in ["_Test Global FY 2001", "_Test Company FY 2001"]:
|
||||
if frappe.db.exists("Fiscal Year", name):
|
||||
frappe.delete_doc("Fiscal Year", name)
|
||||
|
||||
global_fy = frappe.new_doc("Fiscal Year")
|
||||
global_fy.year = "_Test Global FY 2001"
|
||||
global_fy.year_start_date = "2001-04-01"
|
||||
global_fy.year_end_date = "2002-03-31"
|
||||
global_fy.insert()
|
||||
|
||||
company_fy = frappe.new_doc("Fiscal Year")
|
||||
company_fy.year = "_Test Company FY 2001"
|
||||
company_fy.year_start_date = "2001-01-01"
|
||||
company_fy.year_end_date = "2001-12-31"
|
||||
company_fy.append("companies", {"company": "_Test Company"})
|
||||
|
||||
company_fy.insert()
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name))
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name))
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
test_records = [
|
||||
|
||||
@@ -100,7 +100,7 @@ class GLEntry(Document):
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
@@ -137,20 +137,18 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
|
||||
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (
|
||||
@@ -193,6 +191,7 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -206,6 +205,7 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -254,7 +254,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
@@ -274,7 +274,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
@@ -417,16 +417,16 @@ def update_outstanding_amt(
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(company, account, adv_adj=None):
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not role_allowed_for_frozen_entries:
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
@@ -440,7 +440,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = {
|
||||
return [__("Disbursed"), "blue", "status,=,Disbursed"];
|
||||
} else if (doc.status == "Settled") {
|
||||
return [__("Settled"), "orange", "status,=,Settled"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
} else if (doc.status == "Canceled") {
|
||||
return [__("Canceled"), "red", "status,=,Canceled"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,20 +43,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
@@ -125,12 +111,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
|
||||
if (frm.doc.voucher_type !== "Exchange Gain Or Loss") {
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
before_save: function (frm) {
|
||||
if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) {
|
||||
@@ -217,7 +197,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
@@ -268,10 +247,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
});
|
||||
|
||||
var update_jv_details = function (doc, r) {
|
||||
@@ -741,8 +716,6 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
erpnext.journal_entry.clear_fields(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
@@ -767,13 +740,4 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
clear_fields: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
|
||||
row.party_type = null;
|
||||
row.party = null;
|
||||
row.bank_account = null;
|
||||
|
||||
frm.refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
@@ -69,7 +64,6 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -522,7 +516,7 @@
|
||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding "
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
@@ -583,47 +577,6 @@
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -638,7 +591,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
@@ -18,7 +17,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -32,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -49,7 +49,6 @@ class JournalEntry(AccountsController):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -66,7 +65,6 @@ class JournalEntry(AccountsController):
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
is_system_generated: DF.Check
|
||||
@@ -74,8 +72,6 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -87,8 +83,6 @@ class JournalEntry(AccountsController):
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
title: DF.Data | None
|
||||
total_amount: DF.Currency
|
||||
total_amount_currency: DF.Link | None
|
||||
@@ -155,8 +149,8 @@ class JournalEntry(AccountsController):
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
JournalTaxWithholding(self).on_validate()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
@@ -179,17 +173,16 @@ class JournalEntry(AccountsController):
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||
queue_submission(self, "_submit")
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
@@ -200,12 +193,11 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
JournalTaxWithholding(self).on_submit()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
@@ -289,8 +281,6 @@ class JournalEntry(AccountsController):
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# Cancel tax withholding entries
|
||||
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -304,10 +294,8 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
JournalTaxWithholding(self).on_cancel()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -363,6 +351,95 @@ class JournalEntry(AccountsController):
|
||||
StockAccountInvalidTransaction,
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
self.update_asset_on_depreciation()
|
||||
self.update_asset_on_disposal()
|
||||
@@ -375,7 +452,7 @@ class JournalEntry(AccountsController):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_cached_doc("Asset", d.reference_name)
|
||||
@@ -557,36 +634,18 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if (
|
||||
not (d.party_type and d.party) and not self.party_not_required
|
||||
): # skipping validation if party_not_required is passed via payroll entry
|
||||
if not (d.party_type and d.party):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -1178,11 +1237,6 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
@@ -1210,7 +1264,6 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
frappe.flags.party_not_required = False
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
@@ -1656,9 +1709,6 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
|
||||
"party_type": party_type,
|
||||
"account_type": account_details.account_type,
|
||||
"account_currency": account_details.account_currency or company_currency,
|
||||
"bank_account": (
|
||||
frappe.db.get_value("Bank Account", {"account": account, "company": company}) or None
|
||||
),
|
||||
# The date used to retreive the exchange rate here is the date passed in
|
||||
# as an argument to this function. It is assumed to be the date on which the balance is sought
|
||||
"exchange_rate": get_exchange_rate(
|
||||
@@ -1691,6 +1741,8 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1712,8 +1764,8 @@ def get_exchange_rate(
|
||||
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(IntegrationTestCase):
|
||||
@@ -592,15 +591,6 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -107,6 +106,7 @@
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"no_copy": 1,
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -271,8 +271,7 @@
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
@@ -280,21 +279,13 @@
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -11,5 +11,6 @@ def get_data():
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
|
||||
{"items": ["Budget"]},
|
||||
],
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user