Compare commits

..

1 Commits

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

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

The following files were modified:

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

View File

@@ -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

View File

@@ -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

View File

@@ -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.
@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext version -
ERPNext Verion -
validations:
required: true

View File

@@ -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.

View File

@@ -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
-->

View File

@@ -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

View File

@@ -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:

View File

@@ -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: |

View File

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

View File

@@ -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
@@ -43,6 +43,3 @@ jobs:
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
- name: Semgrep for Test Correctness
run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml

View File

@@ -29,7 +29,7 @@ jobs:
services:
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##*/}}"

View File

@@ -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

View File

@@ -4,8 +4,8 @@ on:
workflow_dispatch:
concurrency:
group: server-individual-tests-lightmode-develop
cancel-in-progress: true
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: false
permissions:
contents: read
@@ -17,11 +17,11 @@ 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
matrix=$(find . -path '*/test_*.py' | xargs grep -l 'def test_' | sort | awk '{
matrix=$(find . -path '*/doctype/*/test_*.py' | xargs grep -l 'def test_' | awk '{
# Remove ./ prefix, file extension, and replace / with .
gsub(/^\.\//, "", $0)
gsub(/\.py$/, "", $0)
@@ -58,7 +58,6 @@ jobs:
strategy:
fail-fast: false
matrix: ${{fromJson(needs.discover.outputs.matrix)}}
max-parallel: 14
name: Test
@@ -73,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
@@ -112,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
@@ -131,13 +130,4 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: |
site_name=$(echo "${{matrix.test}}" | sed -e 's/.*\.\(test_.*$\)/\1/')
echo "$site_name"
mkdir ~/frappe-bench/sites/$site_name
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/$site_name/site_config.json
cd ~/frappe-bench/
bench --site $site_name reinstall --yes
bench --site $site_name set-config allow_tests true
bench --site $site_name run-tests --module ${{ matrix.test }} --lightmode
run: 'cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --module ${{ matrix.test }}'

View File

@@ -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

View File

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

View File

@@ -7,7 +7,6 @@ on:
paths-ignore:
- '**.js'
- '**.css'
- '**.svg'
- '**.md'
- '**.html'
- 'crowdin.yml'
@@ -41,7 +40,6 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
@@ -57,7 +55,6 @@ jobs:
mysql:
image: mariadb:10.6
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
@@ -65,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: |
@@ -81,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
@@ -112,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
@@ -131,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 --lightmode --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
env:
TYPE: server
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
- name: Show bench output
@@ -142,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
@@ -150,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

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,5 @@
{
"branches": ["version-16"],
"branches": ["version-13"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
@@ -21,4 +21,4 @@
],
"@semantic-release/github"
]
}
}

View File

@@ -1,4 +1,3 @@
<div align="center">
<a href="https://frappe.io/erpnext">
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
@@ -134,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.

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2018-12-28 22:29:21.828090",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 15,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "fax",
"label": "Tax Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator",
"name": "Address-tax_category",
"no_copy": 0,
"options": "Tax Category",
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2020-10-14 17:41:40.878179",
"default": "0",
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 20,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "linked_with",
"label": "Is Your Company Address",
"length": 0,
"mandatory_depends_on": null,
"modified": "2020-10-14 17:41:40.878179",
"modified_by": "Administrator",
"name": "Address-is_your_company_address",
"no_copy": 0,
"options": null,
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Address",
"property_setters": [],
"sync_on_migrate": 1
}

View File

@@ -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": []
}
}

View File

@@ -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": []
}
}

View File

@@ -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()
@@ -524,8 +524,7 @@ def make_gl_entries(
if gl_entries:
try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
if not frappe.in_test:
frappe.db.commit()
frappe.db.commit()
except Exception as e:
if frappe.in_test:
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
@@ -607,8 +606,7 @@ def book_revenue_via_journal_entry(
if submit:
journal_entry.submit()
if not frappe.in_test:
frappe.db.commit()
frappe.db.commit()
except Exception:
frappe.db.rollback()
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")

View File

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

View File

@@ -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": "2026-04-14 18:14:42.202065",
"modified": "2025-01-22 10:40:35.766017",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -256,17 +248,8 @@
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",
"search_fields": "account_number",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
@@ -274,4 +257,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -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",
}

View File

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

View File

@@ -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",
]

View File

@@ -34,13 +34,6 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -324,21 +317,13 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Expense Account"
"account_type": "Stock Adjustment"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -355,7 +340,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Depreciation"
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -33,17 +33,6 @@
},
"account_number": "1151.000"
},
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1152.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.002",
"account_type": "Tax"
},
"account_number": "1152.000"
},
"account_number": "1150.000"
},
"Kas": {
@@ -108,6 +97,17 @@
},
"account_number": "1130.000"
},
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1151.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.001",
"account_type": "Tax"
},
"account_number": "1150.000"
},
"account_number": "1100.000"
},

View File

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

View File

@@ -1,840 +0,0 @@
{
"name": "Philippines",
"country": "Philippines",
"tree": {
"Asset": {
"account_number": "1000",
"is_group": 1,
"root_type": "Asset",
"Current Assets": {
"account_number": "1001",
"is_group": 1,
"root_type": "Asset",
"Cash": {
"account_number": "1100",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Cash on Hand": {
"account_number": "1101",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
}
}
},
"Bank Accounts": {
"account_number": "1102",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
},
"Advances to Officers & Employees": {
"account_number": "1290",
"is_group": 1,
"root_type": "Asset",
"Advances to Officers & Employees": {
"account_number": "1291",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable Trade": {
"account_number": "1300",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Trade": {
"account_number": "1301",
"is_group": 0,
"root_type": "Asset",
"account_type": "Receivable"
}
},
"Accounts Receivable - Affiliates": {
"account_number": "1310",
"is_group": 1,
"root_type": "Asset",
"Due from Company": {
"account_number": "1311",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable - Others": {
"account_number": "1400",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Others": {
"account_number": "1401",
"is_group": 0,
"root_type": "Asset"
}
},
"Parts, Materials and Supplies": {
"account_number": "1500",
"is_group": 1,
"root_type": "Asset",
"Parts, Materials and Supplies": {
"account_number": "1501",
"is_group": 0,
"root_type": "Asset"
},
"Raw Materials - Demo": {
"account_number": "1502",
"is_group": 0,
"root_type": "Asset"
}
},
"Project in Progress": {
"account_number": "1510",
"is_group": 1,
"root_type": "Asset",
"Project in Progress": {
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
}
},
"Prepayments": {
"account_number": "1600",
"is_group": 1,
"root_type": "Asset",
"Prepaid Insurance & Bonds": {
"account_number": "1601",
"is_group": 0,
"root_type": "Asset"
},
"Prepaid Rent": {
"account_number": "1602",
"is_group": 0,
"root_type": "Asset"
}
},
"VAT Input Tax": {
"account_number": "1610",
"is_group": 1,
"root_type": "Asset",
"VAT Input Tax - Goods": {
"account_number": "1611",
"is_group": 0,
"root_type": "Asset",
"account_type": "Tax"
}
}
},
"Non - Current Assets": {
"account_number": "1002",
"is_group": 1,
"root_type": "Asset",
"Property, Plants And Equipments": {
"account_number": "1700",
"is_group": 1,
"root_type": "Asset",
"Land": {
"account_number": "1701",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Buildings & Improvements": {
"account_number": "1702",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Delivery & Trans Equipment": {
"account_number": "1703",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Furniture & Fixtures": {
"account_number": "1704",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Machinery & Equipment": {
"account_number": "1705",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
}
},
"Accum Depr. - Property, Plants and Equipment": {
"account_number": "1800",
"is_group": 1,
"root_type": "Asset",
"Accumulated Dep Bdgs & Improv": {
"account_number": "1801",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Delivery & Trans": {
"account_number": "1802",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Furniture & Fixture": {
"account_number": "1803",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Depreciation - Machinery & Equipment": {
"account_number": "1804",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
},
"Other Assets": {
"account_number": "1003",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1900",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1901",
"is_group": 0,
"root_type": "Asset"
}
},
"Miscellaneous Deposits": {
"account_number": "1910",
"is_group": 1,
"root_type": "Asset",
"Miscellaneous Deposits": {
"account_number": "1911",
"is_group": 0,
"root_type": "Asset"
}
},
"Retirement Fund": {
"account_number": "1920",
"is_group": 1,
"root_type": "Asset",
"Retirement Fund": {
"account_number": "1921",
"is_group": 0,
"root_type": "Asset"
}
},
"Investment": {
"account_number": "1930",
"is_group": 1,
"root_type": "Asset",
"Investment": {
"account_number": "1931",
"is_group": 0,
"root_type": "Asset"
}
},
"System Development": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
"System Development": {
"account_number": "1941",
"is_group": 0,
"root_type": "Asset"
}
}
}
},
"Liability": {
"account_number": "2000",
"is_group": 1,
"root_type": "Liability",
"Current Liabilities": {
"account_number": "2001",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable Trade": {
"account_number": "2100",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Trade": {
"account_number": "2101",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Accounts Payable Others": {
"account_number": "2110",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Payroll": {
"account_number": "2111",
"is_group": 0,
"root_type": "Liability"
}
},
"VAT Output Tax": {
"account_number": "2200",
"is_group": 1,
"root_type": "Liability",
"VAT Output Tax": {
"account_number": "2201",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Wages": {
"account_number": "2210",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Wages": {
"account_number": "2211",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Expanded": {
"account_number": "2220",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Expanded": {
"account_number": "2221",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Accruals And Other Current Payables": {
"account_number": "2300",
"is_group": 1,
"root_type": "Liability",
"Stock Received But Not Billed": {
"account_number": "2301",
"is_group": 0,
"root_type": "Liability",
"account_type": "Stock Received But Not Billed"
}
},
"Payable to Government and Other Institutions": {
"account_number": "2400",
"is_group": 1,
"root_type": "Liability",
"SSS Premium Payable": {
"account_number": "2401",
"is_group": 0,
"root_type": "Liability"
},
"SSS Salary Loan Payable": {
"account_number": "2402",
"is_group": 0,
"root_type": "Liability"
},
"PhilHealth Premium": {
"account_number": "2403",
"is_group": 0,
"root_type": "Liability"
},
"Pag-ibig Loan Payable": {
"account_number": "2404",
"is_group": 0,
"root_type": "Liability"
},
"Coop Loans": {
"account_number": "2405",
"is_group": 0,
"root_type": "Liability"
},
"Coop Contributions": {
"account_number": "2406",
"is_group": 0,
"root_type": "Liability"
},
"Canteen": {
"account_number": "2407",
"is_group": 0,
"root_type": "Liability"
},
"AUB Loan Payable": {
"account_number": "2408",
"is_group": 0,
"root_type": "Liability"
},
"HSBC Loan Payable": {
"account_number": "2409",
"is_group": 0,
"root_type": "Liability"
}
},
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Non Current Liabilities": {
"account_number": "2002",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2600",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2601",
"is_group": 0,
"root_type": "Liability"
}
},
"Deferred Income": {
"account_number": "2700",
"is_group": 1,
"root_type": "Liability",
"Deferred Income": {
"account_number": "2701",
"is_group": 0,
"root_type": "Liability"
}
},
"Notes Payable": {
"account_number": "2800",
"is_group": 1,
"root_type": "Liability",
"Notes Payable": {
"account_number": "2801",
"is_group": 0,
"root_type": "Liability"
}
},
"Dividends Payable": {
"account_number": "2900",
"is_group": 0,
"root_type": "Liability"
}
}
},
"Equity": {
"account_number": "3000",
"is_group": 1,
"root_type": "Equity",
"STOCKHOLDER'S EQUITY": {
"account_number": "3001",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3100",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3101",
"is_group": 0,
"root_type": "Equity"
}
},
"Subscription Receivable": {
"account_number": "3200",
"is_group": 1,
"root_type": "Equity",
"Subscription Receivable": {
"account_number": "3201",
"is_group": 0,
"root_type": "Equity"
}
},
"Retained Earnings": {
"account_number": "3300",
"is_group": 1,
"root_type": "Equity",
"Retained Earnings": {
"account_number": "3301",
"is_group": 0,
"root_type": "Equity"
}
},
"Current Year (Profit/Loss)": {
"account_number": "3400",
"is_group": 0,
"root_type": "Equity"
},
"Drawings": {
"account_number": "3500",
"is_group": 1,
"root_type": "Equity",
"Drawings": {
"account_number": "3501",
"is_group": 0,
"root_type": "Equity"
}
}
}
},
"Income": {
"account_number": "4000",
"is_group": 1,
"root_type": "Income",
"Gross Sales": {
"account_number": "4100",
"is_group": 1,
"root_type": "Income",
"Sales": {
"account_number": "4101",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Adjustment": {
"account_number": "4200",
"is_group": 1,
"root_type": "Income",
"Sales Return And Allowance": {
"account_number": "4201",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Discount": {
"account_number": "4300",
"is_group": 1,
"root_type": "Income",
"Sales Discount": {
"account_number": "4301",
"is_group": 0,
"root_type": "Income"
}
},
"Other Income": {
"account_number": "6000",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6010",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6011",
"is_group": 0,
"root_type": "Income"
}
},
"Dividend Income": {
"account_number": "6020",
"is_group": 1,
"root_type": "Income",
"Dividend Income": {
"account_number": "6021",
"is_group": 0,
"root_type": "Income"
}
}
}
},
"Expense": {
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,
"root_type": "Expense",
"Salaries, Wages": {
"account_number": "5101",
"is_group": 0,
"root_type": "Expense"
},
"13th Month Pay & Bonus": {
"account_number": "5102",
"is_group": 0,
"root_type": "Expense"
},
"Overtime & Night Diff": {
"account_number": "5103",
"is_group": 0,
"root_type": "Expense"
},
"Incentive/Performance Bonus": {
"account_number": "5104",
"is_group": 0,
"root_type": "Expense"
},
"Employees Benefits": {
"account_number": "5105",
"is_group": 0,
"root_type": "Expense"
},
"Advertising & Promotions": {
"account_number": "5106",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Leasehold Improvement": {
"account_number": "5107",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Pre-Operating": {
"account_number": "5108",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of System Development": {
"account_number": "5109",
"is_group": 0,
"root_type": "Expense"
},
"Audit & Legal Fee": {
"account_number": "5110",
"is_group": 0,
"root_type": "Expense"
},
"Bad Debts Expenses": {
"account_number": "5111",
"is_group": 0,
"root_type": "Expense"
},
"Client Service & Maintenance": {
"account_number": "5112",
"is_group": 0,
"root_type": "Expense"
},
"Commission Expenses": {
"account_number": "5113",
"is_group": 0,
"root_type": "Expense"
},
"Communications": {
"account_number": "5114",
"is_group": 0,
"root_type": "Expense"
},
"Contractual Services": {
"account_number": "5115",
"is_group": 0,
"root_type": "Expense"
},
"Depreciation Expenses": {
"account_number": "5116",
"is_group": 0,
"root_type": "Expense",
"account_type": "Depreciation"
},
"Donation & Contribution": {
"account_number": "5117",
"is_group": 0,
"root_type": "Expense"
},
"Dues & Subscription": {
"account_number": "5118",
"is_group": 0,
"root_type": "Expense"
},
"Employee Med/Dental/Hosp Expenses": {
"account_number": "5119",
"is_group": 0,
"root_type": "Expense"
},
"Employee Uniforms": {
"account_number": "5120",
"is_group": 0,
"root_type": "Expense"
},
"Equipage": {
"account_number": "5121",
"is_group": 0,
"root_type": "Expense"
},
"Expenses for Reclassification": {
"account_number": "5122",
"is_group": 0,
"root_type": "Expense"
},
"Gas & Oil": {
"account_number": "5123",
"is_group": 0,
"root_type": "Expense"
},
"Insurance Expenses": {
"account_number": "5124",
"is_group": 0,
"root_type": "Expense"
},
"Light & Water": {
"account_number": "5125",
"is_group": 0,
"root_type": "Expense"
},
"Local/Overseas Travel": {
"account_number": "5126",
"is_group": 0,
"root_type": "Expense"
},
"Meals & Transportation Expenses": {
"account_number": "5127",
"is_group": 0,
"root_type": "Expense"
},
"Meeting & Conferences": {
"account_number": "5128",
"is_group": 0,
"root_type": "Expense"
},
"Miscellaneous Expenses": {
"account_number": "5129",
"is_group": 0,
"root_type": "Expense"
},
"Mockup Expenses": {
"account_number": "5130",
"is_group": 0,
"root_type": "Expense"
},
"Obsolescence Expenses": {
"account_number": "5131",
"is_group": 0,
"root_type": "Expense"
},
"Other Support Cost": {
"account_number": "5132",
"is_group": 0,
"root_type": "Expense"
},
"Pag-ibig Contribution": {
"account_number": "5133",
"is_group": 0,
"root_type": "Expense"
},
"Performance Bonds": {
"account_number": "5134",
"is_group": 0,
"root_type": "Expense"
},
"Pre Employment Expenses": {
"account_number": "5135",
"is_group": 0,
"root_type": "Expense"
},
"Professional Fees": {
"account_number": "5136",
"is_group": 0,
"root_type": "Expense"
},
"Recruitment & Employment": {
"account_number": "5137",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses": {
"account_number": "5138",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses Others": {
"account_number": "5139",
"is_group": 0,
"root_type": "Expense"
},
"Repairs & Maintenance": {
"account_number": "5140",
"is_group": 0,
"root_type": "Expense"
},
"Representation Expenses": {
"account_number": "5141",
"is_group": 0,
"root_type": "Expense"
},
"Research & Development": {
"account_number": "5142",
"is_group": 0,
"root_type": "Expense"
},
"Security Expenses": {
"account_number": "5143",
"is_group": 0,
"root_type": "Expense"
},
"Shared Services Fee": {
"account_number": "5144",
"is_group": 0,
"root_type": "Expense"
},
"SSS/Medicare/EC Contributions": {
"account_number": "5145",
"is_group": 0,
"root_type": "Expense"
},
"Stationery & Supplies": {
"account_number": "5146",
"is_group": 0,
"root_type": "Expense"
},
"Taxes & Licenses": {
"account_number": "5147",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Training & Seminar": {
"account_number": "5148",
"is_group": 0,
"root_type": "Expense"
}
},
"Stock Adjustment": {
"account_number": "5200",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"
}
}
}
}

View File

@@ -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",
},
}

View File

@@ -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",
},

View File

@@ -3,6 +3,7 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import (
@@ -11,10 +12,11 @@ from erpnext.accounts.doctype.account.account import (
update_account_number,
)
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Company"]
class TestAccount(ERPNextTestSuite):
class TestAccount(IntegrationTestCase):
def test_rename_account(self):
if not frappe.db.exists("Account", "1210 - Debtors - _TC"):
acc = frappe.new_doc("Account")
@@ -321,6 +323,72 @@ class TestAccount(ERPNextTestSuite):
self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.tests.utils import make_test_objects
accounts = [
# [account_name, parent_account, is_group]
["_Test Bank", "Bank Accounts", 0, "Bank", None],
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account CST", "Direct Expenses", 0, "Tax", None],
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
for company, abbr in [
["_Test Company", "_TC"],
["_Test Company 1", "_TC1"],
["_Test Company with perpetual inventory", "TCP1"],
]:
test_objects = make_test_objects(
"Account",
[
{
"doctype": "Account",
"account_name": account_name,
"parent_account": parent_account + " - " + abbr,
"company": company,
"is_group": is_group,
"account_type": account_type,
"account_currency": currency,
}
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects
def get_inventory_account(company, warehouse=None):
account = None
if warehouse:
@@ -347,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()

View File

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

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Account Category", {
// refresh(frm) {
// },
// });

View File

@@ -1,91 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:account_category_name",
"creation": "2025-08-02 06:22:31.835063",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"account_category_name",
"root_type",
"column_break_qluu",
"description"
],
"fields": [
{
"fieldname": "account_category_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Category Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "column_break_qluu",
"fieldtype": "Column Break"
},
{
"fieldname": "root_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Account",
"link_fieldname": "account_category"
}
],
"modified": "2026-03-05 06:49:34.430723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Category",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Auditor"
}
],
"row_format": "Dynamic",
"search_fields": "account_category_name, root_type",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,95 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import os
import frappe
from frappe import _
from frappe.model.document import Document, bulk_insert
DOCTYPE = "Account Category"
class AccountCategory(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
account_category_name: DF.Data
description: DF.SmallText | None
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
# end: auto-generated types
def after_rename(self, old_name, new_name, merge):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FormulaFieldUpdater,
)
# get all template rows with this account category being used
row = frappe.qb.DocType("Financial Report Row")
rows = frappe._dict(
frappe.qb.from_(row)
.select(row.name, row.calculation_formula)
.where(row.calculation_formula.like(f"%{old_name}%"))
.run()
)
if not rows:
return
# Update formulas with new name
updater = FormulaFieldUpdater(
field_name="account_category",
value_mapping={old_name: new_name},
exclude_operators=["like", "not like"],
)
updated_formulas = updater.update_in_rows(rows)
if updated_formulas:
frappe.msgprint(
_("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
)
def import_account_categories(template_path: str):
categories_file = os.path.join(template_path, "account_categories.json")
if not os.path.exists(categories_file):
return
with open(categories_file) as f:
categories = json.load(f, object_hook=frappe._dict)
create_account_categories(categories)
def create_account_categories(categories: list[dict]):
if not categories:
return
existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
new_categories = []
for category_data in categories:
category_name = category_data.get("account_category_name")
if not category_name or category_name in existing_categories:
continue
doc = frappe.get_doc(
{
**category_data,
"doctype": DOCTYPE,
"name": category_name,
}
)
new_categories.append(doc)
existing_categories.add(category_name)
if new_categories:
bulk_insert(DOCTYPE, new_categories)

View File

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

View File

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

View File

@@ -43,7 +43,6 @@ class AccountingDimension(Document):
def validate(self):
self.validate_doctype()
validate_column_name(self.fieldname)
self.validate_fieldname_conflict()
self.validate_dimension_defaults()
def validate_doctype(self):
@@ -75,27 +74,6 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message)
def validate_fieldname_conflict(self):
conflicting_doctypes = []
for doctype in get_doctypes_with_dimensions():
meta = frappe.get_meta(doctype, cached=False)
if any(f.fieldname == self.fieldname for f in meta.get("fields")):
conflicting_doctypes.append(doctype)
if conflicting_doctypes:
frappe.msgprint(
_(
"Fieldname {0} already exists in the following doctypes: {1}. "
"A separate dimension field will not be added to these doctypes. "
"GL Entries will use the value of the existing field as the dimension value."
).format(
frappe.bold(self.fieldname),
", ".join(frappe.bold(d) for d in conflicting_doctypes),
),
title=_("Fieldname Conflict"),
indicator="orange",
)
def validate_dimension_defaults(self):
companies = []
for default in self.get("dimension_defaults"):
@@ -104,7 +82,7 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def on_update(self):
def after_insert(self):
if frappe.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
@@ -125,6 +103,10 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
@@ -259,26 +241,34 @@ def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True):
accounting_dimensions = frappe.get_all(
"Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters={"disabled": 0},
)
def get_accounting_dimensions(as_list=True, filters=None):
if not filters:
filters = {"disabled": 0}
if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all(
"Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters=filters,
)
if as_list:
return [d.fieldname for d in accounting_dimensions]
return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
return accounting_dimensions
return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts():
return frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
if frappe.flags.accounting_dimensions_details is None:
# nosemgrep
frappe.flags.accounting_dimensions_details = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent AND p.disabled = 0""",
as_dict=1,
)
as_dict=1,
)
return frappe.flags.accounting_dimensions_details
def get_dimension_with_children(doctype, dimensions):
@@ -319,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"},
]
)

View File

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

View File

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

View File

@@ -5,13 +5,19 @@ import unittest
import frappe
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Location", "Cost Center", "Department"]
class TestAccountingDimensionFilter(ERPNextTestSuite):
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
create_dimension()
create_accounting_dimension_filter()
self.invoice_list = []
@@ -38,6 +44,17 @@ class TestAccountingDimensionFilter(ERPNextTestSuite):
self.assertRaises(MandatoryAccountDimensionError, si.submit)
self.invoice_list.append(si)
def tearDown(self):
disable_dimension_filter()
disable_dimension()
frappe.flags.accounting_dimensions_details = None
frappe.flags.dimension_filter_map = None
for si in self.invoice_list:
si.load_from_db()
if si.docstatus == 1:
si.cancel()
def create_accounting_dimension_filter():
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}):

View File

@@ -11,8 +11,6 @@
"end_date",
"column_break_4",
"company",
"disabled",
"exempted_role",
"section_break_7",
"closed_documents"
],
@@ -20,6 +18,7 @@
{
"fieldname": "period_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Period Name",
"reqd": 1,
"unique": 1
@@ -50,13 +49,6 @@
"options": "Company",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
@@ -67,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": "2026-03-09 17:15:33.577217",
"modified": "2024-03-27 13:05:57.388109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -122,9 +105,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -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
@@ -97,7 +95,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.asset_type == "Existing Asset":
if doc.is_existing_asset:
return
else:
date = doc.available_for_use_date
@@ -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"])

View File

@@ -3,6 +3,7 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
@@ -10,10 +11,11 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import (
OverlapError,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item"]
class TestAccountingPeriod(ERPNextTestSuite):
class TestAccountingPeriod(IntegrationTestCase):
def test_overlap(self):
ap1 = create_accounting_period(
start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
@@ -35,58 +37,9 @@ class TestAccountingPeriod(ERPNextTestSuite):
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)
def create_accounting_period(**args):
@@ -98,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

View File

@@ -2,15 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {
frm.set_query("document_type", "repost_allowed_types", function (doc, cdt, cdn) {
return {
filters: {
name: ["in", frappe.boot.sysdefaults.repost_allowed_doctypes],
},
};
});
},
refresh: function (frm) {},
enable_immutable_ledger: function (frm) {
if (!frm.doc.enable_immutable_ledger) {
return;
@@ -38,6 +30,16 @@ frappe.ui.form.on("Accounts Settings", {
add_taxes_from_item_tax_template(frm) {
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
},
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
});
function toggle_tax_settings(frm, field_name) {

View File

@@ -16,15 +16,10 @@
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
"enable_subscription",
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date",
"analytics_section",
"enable_accounting_dimensions",
"column_break_vtnr",
"enable_discounts_and_margin",
"journals_section",
"merge_similar_account_heads",
"deferred_accounting_settings_section",
@@ -56,19 +51,12 @@
"allow_pegged_currencies_exchange_rates",
"column_break_yuug",
"stale_days",
"payments_tab",
"section_break_jpd0",
"auto_reconcile_payments",
"auto_reconciliation_job_trigger",
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"repost_section",
"repost_allowed_types",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
"fetch_payment_schedule_in_payment_request",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -76,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",
@@ -94,22 +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",
@@ -208,7 +214,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order/Quotation"
"label": "Automatically Fetch Payment Terms from Order"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -290,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\" rel=\"noopener noreferrer\">Common Party</a>",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
@@ -330,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",
@@ -344,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",
@@ -354,7 +377,7 @@
{
"fieldname": "tab_break_dpet",
"fieldtype": "Tab Break",
"label": "Others"
"label": "Chart Of Accounts"
},
{
"default": "1",
@@ -398,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",
@@ -473,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"
@@ -517,12 +550,16 @@
"label": "Posting Date Inheritance for Exchange Gain / Loss",
"options": "Invoice\nPayment\nReconciliation Date"
},
{
"fieldname": "column_break_xrnd",
"fieldtype": "Column Break"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor"
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -556,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",
@@ -591,133 +639,27 @@
"fieldname": "column_break_ntmi",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
},
{
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
},
{
"default": "0",
"fieldname": "use_legacy_budget_controller",
"fieldtype": "Check",
"label": "Use Legacy Budget Controller"
},
{
"default": "1",
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
},
{
"description": "Users with this role will be notified if the asset depreciation gets failed",
"fieldname": "role_to_notify_on_depreciation_failure",
"fieldtype": "Link",
"label": "Role to Notify on Depreciation Failure",
"options": "Role"
},
{
"default": "30, 60, 90, 120",
"fieldname": "default_ageing_range",
"fieldtype": "Data",
"label": "Default Ageing Range"
},
{
"fieldname": "chart_of_accounts_section",
"fieldtype": "Section Break",
"label": "Chart Of Accounts"
},
{
"fieldname": "banking_section",
"fieldtype": "Section Break",
"label": "Banking"
},
{
"fieldname": "payment_request_section",
"fieldtype": "Section Break",
"label": "Payment Request"
},
{
"fieldname": "budget_section",
"fieldtype": "Section Break",
"label": "Budget"
},
{
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytical Accounting"
},
{
"fieldname": "column_break_vtnr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Apply discounts and margins on products",
"fieldname": "enable_discounts_and_margin",
"fieldtype": "Check",
"label": "Enable Discounts and Margin"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "payment_options_section",
"fieldtype": "Section Break",
"label": "Payment Options"
},
{
"default": "0",
"fieldname": "enable_loyalty_point_program",
"fieldtype": "Check",
"label": "Enable Loyalty Point Program"
},
{
"fieldname": "column_break_ctam",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable cost center, projects and other custom accounting dimensions",
"fieldname": "enable_accounting_dimensions",
"fieldtype": "Check",
"label": "Enable Accounting Dimensions"
},
{
"default": "1",
"description": "Enable Subscription tracking in invoice",
"fieldname": "enable_subscription",
"fieldtype": "Check",
"label": "Enable Subscription"
},
{
"default": "1",
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
},
{
"fieldname": "repost_section",
"fieldtype": "Section Break",
"label": "Repost"
},
{
"fieldname": "repost_allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-05-18 12:16:33.679345",
"modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -10,32 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import sync_auto_reconcile_config
SELLING_DOCTYPES = [
"Sales Invoice",
"Sales Order",
"Delivery Note",
"Quotation",
"Sales Invoice Item",
"Sales Order Item",
"Delivery Note Item",
"Quotation Item",
"POS Invoice",
"POS Invoice Item",
]
BUYING_DOCTYPES = [
"Purchase Invoice",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice Item",
"Purchase Order Item",
"Purchase Receipt Item",
]
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
@@ -47,8 +23,7 @@ class AccountsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -67,20 +42,15 @@ class AccountsSettings(Document):
confirm_before_resetting_posting_date: DF.Check
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_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_payment_schedule_in_payment_request: DF.Check
fetch_valuation_rate_for_internal_transaction: DF.Check
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
@@ -89,12 +59,11 @@ class AccountsSettings(Document):
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
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
repost_allowed_types: DF.Table[RepostAllowedTypes]
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
@@ -105,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):
@@ -131,27 +99,13 @@ 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.enable_accounting_dimensions != self.enable_accounting_dimensions:
toggle_accounting_dimension_sections(not self.enable_accounting_dimensions)
clear_cache = True
if old_doc.enable_discounts_and_margin != self.enable_discounts_and_margin:
toggle_sales_discount_section(not self.enable_discounts_and_margin)
clear_cache = True
if old_doc.enable_loyalty_point_program != self.enable_loyalty_point_program:
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if old_doc.enable_subscription != self.enable_subscription:
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts()
if clear_cache:
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
self.update_property_for_accounting_dimension()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -174,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 (
@@ -198,61 +156,10 @@ class AccountsSettings(Document):
title=_("Auto Tax Settings Error"),
)
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.repost_allowed_types]
if not doctypes:
return
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
for doctype in accounting_dimension_doctypes:
create_property_setter_for_hiding_field(doctype, "accounting_dimensions_section", hide)
def toggle_sales_discount_section(hide):
for doctype in SELLING_DOCTYPES + BUYING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("additional_discount_section"):
create_property_setter_for_hiding_field(doctype, "additional_discount_section", hide)
if meta.has_field("discount_and_margin"):
create_property_setter_for_hiding_field(doctype, "discount_and_margin", hide)
def toggle_loyalty_point_program_section(hide):
for doctype in SELLING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("loyalty_points_redemption"):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def toggle_subscription_sections(hide):
subscription_doctypes = frappe.get_hooks("subscription_doctypes")
for doctype in subscription_doctypes:
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
field_name,
"hidden",
hide,
"Check",
validate_fields_for_doctype=False,
)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
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}")

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,8 @@
"voucher_no",
"against_voucher_type",
"against_voucher_no",
"currency",
"exchange_rate",
"amount",
"base_amount",
"currency",
"event",
"delinked"
],
@@ -50,7 +48,6 @@
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "currency",
"read_only": 1
},
{
@@ -79,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",

View File

@@ -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"],
)

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# 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

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -52,7 +52,6 @@
"fieldtype": "Link",
"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",

View File

@@ -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:
@@ -116,7 +112,6 @@ def get_default_company_bank_account(company, party_type, party):
@frappe.whitelist()
def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

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

View File

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

View File

@@ -3,8 +3,8 @@
# import frappe
import unittest
from erpnext.tests.utils import ERPNextTestSuite
from frappe.tests import IntegrationTestCase
class TestBankAccountType(ERPNextTestSuite):
class TestBankAccountType(IntegrationTestCase):
pass

View File

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

View File

@@ -3,6 +3,7 @@
import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -14,12 +15,13 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(ERPNextTestSuite):
def setUp(self):
frappe.clear_cache()
class TestBankClearance(IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
create_warehouse(
warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},

View File

@@ -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": []
}
}

View File

@@ -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: {

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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()
@@ -139,8 +132,6 @@ class BankTransaction(Document):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry)
@@ -316,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():
@@ -375,12 +332,11 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"),
as_dict=True,
)
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt_bank_account != gl_bank_account:
if bt.bank_account != gl_bank_account:
frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt_bank_account, payment_entry.payment_entry, gl_bank_account
bt.bank_account, payment_entry.payment_entry, gl_bank_account
)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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
@@ -19,17 +17,19 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBudget(ERPNextTestSuite):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.make_monthly_distribution()
cls.make_projects()
def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
self.company = "_Test Company"
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
self.account = "_Test Account Cost for Goods Sold - _TC"
self.cost_center = "_Test Cost Center - _TC"
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",
@@ -50,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",
@@ -74,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",
@@ -106,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",
@@ -150,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
@@ -175,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",
@@ -201,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",
@@ -218,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"})
@@ -238,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
@@ -267,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
@@ -299,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",
@@ -338,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",
@@ -382,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",
@@ -402,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",
@@ -440,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":
@@ -607,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:
@@ -662,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")
@@ -697,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
@@ -733,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

View File

@@ -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": []
}

View File

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

View File

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

View File

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

View File

@@ -126,7 +126,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:15:27.367298",
"modified": "2025-01-22 10:46:42.904001",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -173,20 +173,11 @@
"role": "Employee",
"select": 1,
"share": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

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