mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-17 12:18:49 +00:00
Compare commits
1 Commits
coderabbit
...
copilot/ov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d82ffdde28 |
@@ -45,9 +45,3 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||
|
||||
# these commits actually changed something valuable
|
||||
# but they have a lot of whitespace changes that make blame noisy
|
||||
# PR: https://github.com/frappe/erpnext/pull/49816
|
||||
3ffd50c772735877b330d010c1058f623da8721d
|
||||
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
||||
4
.github/workflows/docs-checker.yml
vendored
4
.github/workflows/docs-checker.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Docs
|
||||
env:
|
||||
|
||||
13
.github/workflows/generate-pot-file.yml
vendored
13
.github/workflows/generate-pot-file.yml
vendored
@@ -15,25 +15,20 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop", "version-16-hotfix"]
|
||||
branch: ["develop"]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
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"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
|
||||
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15", "16"]
|
||||
version: ["14", "15"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Download Semgrep rules
|
||||
|
||||
25
.github/workflows/patch.yml
vendored
25
.github/workflows/patch.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:11.8
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -49,17 +49,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: |
|
||||
3.11
|
||||
3.13
|
||||
3.14
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -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,15 +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 16 3.14
|
||||
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##*/}}"
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/run-indinvidual-tests.yml
vendored
12
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- id: set-matrix
|
||||
run: |
|
||||
# Use grep and find to get the list of test files
|
||||
@@ -72,17 +72,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
4
.github/workflows/semantic-commits.yml
vendored
4
.github/workflows/semantic-commits.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
name: Check Commit Titles
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 200
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
18
.github/workflows/server-tests-mariadb.yml
vendored
18
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.svg'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
@@ -63,12 +62,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -79,9 +78,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -129,9 +128,10 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
||||
env:
|
||||
TYPE: server
|
||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
|
||||
- name: Show bench output
|
||||
@@ -140,6 +140,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
@@ -148,9 +149,10 @@ jobs:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
10
.github/workflows/server-tests-postgres.yml
vendored
10
.github/workflows/server-tests-postgres.yml
vendored
@@ -47,12 +47,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
@@ -50,15 +50,6 @@ pull_request_rules:
|
||||
- version-15-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: backport to version-16-hotfix
|
||||
conditions:
|
||||
- label="backport version-16-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-16-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "17.0.0-dev"
|
||||
__version__ = "16.0.0-dev"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -9,20 +9,18 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2026-01-02 13:01:24.037552",
|
||||
"modified": "2026-01-02 13:04:57.850305",
|
||||
"last_synced_on": "2020-07-22 12:19:59.879476",
|
||||
"modified": "2020-07-22 12:21:48.780513",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Balance",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"source": "Account Balance Timeline",
|
||||
"time_interval": "Monthly",
|
||||
"timeseries": 1,
|
||||
"time_interval": "Quarterly",
|
||||
"timeseries": 0,
|
||||
"timespan": "Last Year",
|
||||
"type": "Line",
|
||||
"use_report_chart": 0,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"chart_type": "Report",
|
||||
"creation": "2025-04-01 20:38:16.986176",
|
||||
"creation": "2020-07-17 11:25:34.448572",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
|
||||
@@ -9,7 +9,7 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2025-12-19 12:37:31.673782",
|
||||
"modified": "2023-07-19 13:08:56.470390",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss",
|
||||
@@ -17,9 +17,8 @@
|
||||
"owner": "Administrator",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"timeseries": 0,
|
||||
"type": "Line",
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,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,
|
||||
@@ -449,12 +449,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()
|
||||
|
||||
@@ -93,10 +93,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):
|
||||
@@ -305,10 +303,10 @@ class Account(NestedSet):
|
||||
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):
|
||||
|
||||
@@ -70,7 +70,6 @@ frappe.treeview_settings["Account"] = {
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -161,14 +160,6 @@ frappe.treeview_settings["Account"] = {
|
||||
.options,
|
||||
description: __("Optional. This setting will be used to filter in various transactions."),
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "account_category",
|
||||
label: __("Account Category"),
|
||||
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
|
||||
.options,
|
||||
description: __("Optional. Used with Financial Report Template"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "tax_rate",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -228,7 +228,6 @@ def get():
|
||||
},
|
||||
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
"account_number": "5000",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
@@ -414,13 +415,15 @@ def create_account(**kwargs):
|
||||
return account.name
|
||||
else:
|
||||
account = frappe.get_doc(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
dict(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
)
|
||||
|
||||
account.save()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"exempted_role",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -68,18 +67,10 @@
|
||||
"label": "Closed Documents",
|
||||
"options": "Closed Document",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Role allowed to bypass period restrictions.",
|
||||
"fieldname": "exempted_role",
|
||||
"fieldtype": "Link",
|
||||
"label": "Exempted Role",
|
||||
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-12-01 16:53:44.631299",
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
|
||||
@@ -30,7 +30,6 @@ class AccountingPeriod(Document):
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
exempted_role: DF.Link | None
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
# end: auto-generated types
|
||||
@@ -114,7 +113,7 @@ 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)
|
||||
@@ -127,11 +126,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"])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
@@ -36,59 +37,6 @@ class TestAccountingPeriod(IntegrationTestCase):
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def test_accounting_period_exempted_role(self):
|
||||
# Create Accounting Period with exempted role
|
||||
ap = create_accounting_period(
|
||||
period_name="Test Accounting Period Exempted",
|
||||
exempted_role="Accounts Manager",
|
||||
start_date="2025-12-01",
|
||||
end_date="2025-12-31",
|
||||
)
|
||||
ap.save()
|
||||
|
||||
# Create users
|
||||
users = frappe.get_all("User", filters={"email": ["like", "test%"]}, limit=1)
|
||||
user = None
|
||||
|
||||
if users[0].name:
|
||||
user = frappe.get_doc("User", users[0].name)
|
||||
else:
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": "test1@example.com",
|
||||
"first_name": "Test1",
|
||||
}
|
||||
)
|
||||
user.insert()
|
||||
|
||||
user.roles = []
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
|
||||
# ---- Non-exempted user should FAIL ----
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
frappe.set_user(user.name)
|
||||
posting_date = "2025-12-11"
|
||||
doc = create_sales_invoice(
|
||||
do_not_save=1,
|
||||
posting_date=posting_date,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
doc.submit()
|
||||
|
||||
# ---- Exempted role should PASS ----
|
||||
user.append("roles", {"role": "Accounts Manager"})
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
doc = create_sales_invoice(do_not_save=1, posting_date=posting_date)
|
||||
|
||||
doc.submit() # Should not raise
|
||||
self.assertEqual(doc.docstatus, 1)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
@@ -103,6 +51,5 @@ def create_accounting_period(**args):
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
accounting_period.exempted_role = args.exempted_role or ""
|
||||
|
||||
return accounting_period
|
||||
|
||||
@@ -64,17 +64,27 @@
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"role_to_notify_on_depreciation_failure",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
@@ -82,23 +92,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",
|
||||
"budget_settings",
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -279,9 +297,16 @@
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -319,6 +344,11 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -333,6 +363,11 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
@@ -343,7 +378,7 @@
|
||||
{
|
||||
"fieldname": "tab_break_dpet",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Others"
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -387,6 +422,11 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
@@ -462,9 +502,14 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
@@ -506,6 +551,10 @@
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
@@ -545,6 +594,11 @@
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -604,49 +658,15 @@
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "30, 60, 90, 120",
|
||||
"fieldname": "default_ageing_range",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Ageing Range"
|
||||
},
|
||||
{
|
||||
"fieldname": "chart_of_accounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Budget"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified": "2025-10-20 14:06:08.870427",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -22,6 +23,7 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
@@ -40,7 +42,6 @@ class AccountsSettings(Document):
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
@@ -49,6 +50,7 @@ class AccountsSettings(Document):
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
@@ -57,11 +59,11 @@ class AccountsSettings(Document):
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
@@ -98,6 +100,9 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -124,6 +129,10 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
@@ -152,5 +161,6 @@ class AccountsSettings(Document):
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal file
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-25 10:24:39.836195",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_detail",
|
||||
"account_head",
|
||||
"allocated_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Name",
|
||||
"options": "reference_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_detail",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Head",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "party_account_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:58.308002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Tax",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BudgetDistribution(Document):
|
||||
class AdvanceTax(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -14,13 +14,14 @@ class BudgetDistribution(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
end_date: DF.Date | None
|
||||
account_head: DF.Link | None
|
||||
allocated_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
percent: DF.Percent
|
||||
start_date: DF.Date | None
|
||||
reference_detail: DF.Data | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -14,7 +14,6 @@
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"is_tax_withholding_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -26,6 +25,7 @@
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"allocated_amount",
|
||||
"column_break_13",
|
||||
"base_tax_amount",
|
||||
"base_net_amount",
|
||||
@@ -97,11 +97,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -172,6 +172,12 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
@@ -207,26 +213,18 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:42:18.707671",
|
||||
"modified": "2024-11-22 19:16:22.346267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class AdvanceTaxesandCharges(Document):
|
||||
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
allocated_amount: DF.Currency
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -27,12 +28,10 @@ class AdvanceTaxesandCharges(Document):
|
||||
currency: DF.Link | None
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -42,4 +42,8 @@ frappe.ui.form.on("Bank Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
is_company_account: function (frm) {
|
||||
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -99,7 +98,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -254,7 +252,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -51,29 +51,25 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_is_company_account()
|
||||
self.validate_company()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_is_company_account(self):
|
||||
if self.is_company_account:
|
||||
if not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
if not self.account:
|
||||
frappe.throw(_("Company Account is mandatory"))
|
||||
|
||||
self.validate_account()
|
||||
|
||||
def validate_account(self):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
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
|
||||
@@ -125,7 +125,7 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
msg += "</ul>"
|
||||
msgprint(_(msg))
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
@@ -134,44 +134,16 @@ class BankClearance(Document):
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
old_clearance_date = frappe.db.get_value(
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{
|
||||
"parent": d.payment_entry,
|
||||
"account": self.account,
|
||||
"amount": [">", 0],
|
||||
},
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
if d.clearance_date or old_clearance_date:
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
sales_invoice = frappe.get_lazy_doc("Sales Invoice", d.payment_entry)
|
||||
sales_invoice.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
old_clearance_date = payment_entry.clearance_date
|
||||
|
||||
if d.clearance_date or old_clearance_date:
|
||||
# using db_set to trigger notification
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
payment_entry.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -371,7 +371,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",
|
||||
)
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -236,32 +233,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-10-23 17:32:58.514807",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,8 +32,6 @@ 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
|
||||
@@ -47,14 +45,9 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -314,40 +307,6 @@ class BankTransaction(Document):
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
def validate_included_fee(self):
|
||||
"""
|
||||
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||
"""
|
||||
|
||||
if self.included_fee and self.withdrawal:
|
||||
if self.included_fee > self.withdrawal:
|
||||
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||
|
||||
def handle_excluded_fee(self):
|
||||
# Include the excluded fee on validate to handle all further processing the same
|
||||
excluded_fee = flt(self.excluded_fee)
|
||||
if excluded_fee <= 0:
|
||||
return
|
||||
|
||||
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||
|
||||
# Enforce directionality
|
||||
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||
frappe.throw(
|
||||
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||
)
|
||||
|
||||
if flt(self.deposit) > 0:
|
||||
self.deposit = flt(self.deposit) - excluded_fee
|
||||
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||
elif flt(self.withdrawal) >= 0:
|
||||
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||
self.excluded_fee = 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(UnitTestCase):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 101
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||
|
||||
def test_included_fee_allows_equal(self):
|
||||
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||
amount (only the fee was deducted from the account)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 100
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_included_fee_allows_for_deposit(self):
|
||||
"""For deposits, a fee may be recorded separately without limiting the
|
||||
received amount."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.included_fee = 999
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_excluded_fee_noop_when_zero(self):
|
||||
"""When there is no excluded fee to apply, the amounts should remain
|
||||
unchanged."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 5
|
||||
bt.excluded_fee = 0
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 100)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.excluded_fee = 11
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||
"""A transaction must be either incoming or outgoing when applying a
|
||||
fee, not both."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.withdrawal = 10
|
||||
bt.excluded_fee = 1
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_deducts_from_deposit(self):
|
||||
"""When a fee is deducted from an incoming payment, the net received
|
||||
amount decreases and the fee is tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 95)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||
while still tracking the fee."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 5
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_increases_outgoing_payment(self):
|
||||
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||
the total money leaving increases and the fee is tracked."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 105)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||
"""If only an excluded fee is provided, it should be treated as an
|
||||
outgoing payment and the fee is then tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 5)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
@@ -4,19 +4,20 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
frappe.ui.form.on("Budget", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("monthly_distribution", function () {
|
||||
frm.set_query("account", "accounts", function () {
|
||||
return {
|
||||
filters: {
|
||||
fiscal_year: frm.doc.fiscal_year,
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account", function () {
|
||||
frm.set_query("monthly_distribution", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
fiscal_year: frm.doc.fiscal_year,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -29,20 +30,8 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
},
|
||||
|
||||
refresh: async function (frm) {
|
||||
refresh: function (frm) {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
budget_against: function (frm) {
|
||||
@@ -50,20 +39,6 @@ frappe.ui.form.on("Budget", {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
},
|
||||
|
||||
budget_amount(frm) {
|
||||
if (frm.doc.budget_distribution?.length) {
|
||||
frm.doc.budget_distribution.forEach((row) => {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
});
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
|
||||
distribute_equally: function (frm) {
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
set_null_value: function (frm) {
|
||||
if (frm.doc.budget_against == "Cost Center") {
|
||||
frm.set_value("project", null);
|
||||
@@ -76,68 +51,4 @@ frappe.ui.form.on("Budget", {
|
||||
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
|
||||
frm.toggle_reqd("project", frm.doc.budget_against == "Project");
|
||||
},
|
||||
|
||||
revise_budget_action: function (frm) {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created."
|
||||
),
|
||||
function () {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.budget.budget.revise_budget",
|
||||
args: { budget_name: frm.doc.name },
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__("New revised budget created successfully"));
|
||||
frappe.set_route("Form", "Budget", r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
function () {
|
||||
frappe.msgprint(__("Revision cancelled"));
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Budget Distribution", {
|
||||
amount(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
percent(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function set_total_budget_amount(frm) {
|
||||
let total = 0;
|
||||
|
||||
(frm.doc.budget_distribution || []).forEach((row) => {
|
||||
total += flt(row.amount);
|
||||
});
|
||||
|
||||
frm.set_value("budget_distribution_total", total);
|
||||
}
|
||||
|
||||
function toggle_distribution_fields(frm) {
|
||||
const grid = frm.fields_dict.budget_distribution.grid;
|
||||
|
||||
["amount", "percent"].forEach((field) => {
|
||||
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
|
||||
});
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
|
||||
@@ -12,23 +12,10 @@
|
||||
"company",
|
||||
"cost_center",
|
||||
"project",
|
||||
"account",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
"monthly_distribution",
|
||||
"amended_from",
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_start_date",
|
||||
"budget_end_date",
|
||||
"distribution_frequency",
|
||||
"budget_amount",
|
||||
"section_break_nwug",
|
||||
"distribute_equally",
|
||||
"section_break_fpdt",
|
||||
"budget_distribution",
|
||||
"section_break_wkqb",
|
||||
"column_break_paum",
|
||||
"column_break_nwor",
|
||||
"budget_distribution_total",
|
||||
"section_break_6",
|
||||
"applicable_on_material_request",
|
||||
"action_if_annual_budget_exceeded_on_mr",
|
||||
@@ -45,8 +32,8 @@
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_kkan",
|
||||
"revision_of"
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -57,7 +44,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Budget Against",
|
||||
"options": "\nCost Center\nProject",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -67,7 +53,6 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -77,8 +62,7 @@
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.budget_against == 'Project'",
|
||||
@@ -86,13 +70,28 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
|
||||
"fieldname": "monthly_distribution",
|
||||
"fieldtype": "Link",
|
||||
"label": "Monthly Distribution",
|
||||
"options": "Monthly Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -188,12 +187,22 @@
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"default": "BUDGET-.########",
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Accounts",
|
||||
"options": "Budget Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.########",
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
@@ -223,117 +232,13 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fpdt",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Distribution",
|
||||
"options": "Budget Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_kkan",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "revision_of",
|
||||
"fieldtype": "Data",
|
||||
"label": "Revision Of",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "distribute_equally",
|
||||
"fieldtype": "Check",
|
||||
"label": "Distribute Equally"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_nwug",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "To Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_start_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_end_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget End Date"
|
||||
},
|
||||
{
|
||||
"default": "Monthly",
|
||||
"fieldname": "distribution_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Distribution Frequency",
|
||||
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wkqb",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_paum",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nwor",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Distribution Total",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-10 02:35:01.197613",
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
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
|
||||
from frappe.utils.data import get_first_day
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -32,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"]
|
||||
@@ -49,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":
|
||||
@@ -186,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 = [
|
||||
@@ -429,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,
|
||||
@@ -509,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,
|
||||
@@ -523,41 +257,38 @@ 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
|
||||
if args.actual_expense > budget_amount:
|
||||
diff = args.actual_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
|
||||
else:
|
||||
diff = total_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
|
||||
|
||||
currency = frappe.get_cached_value("Company", params.company, "default_currency")
|
||||
currency = frappe.get_cached_value("Company", args.company, "default_currency")
|
||||
msg = _msg.format(
|
||||
_(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)),
|
||||
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
|
||||
@@ -570,25 +301,14 @@ def compare_expense_with_budget(params, budget_amount, action_for, action, budge
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(params, currency, budget_against):
|
||||
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
params.budget_against_field: budget_against,
|
||||
"account": params.account,
|
||||
"company": params.company,
|
||||
}
|
||||
)
|
||||
|
||||
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
gl_filters = common_filters.copy()
|
||||
gl_filters.update(
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"is_cancelled": 0,
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -597,23 +317,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>"
|
||||
@@ -622,24 +337,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(
|
||||
@@ -647,34 +360,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
|
||||
@@ -688,9 +409,9 @@ def get_requested_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Purchase Order")
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
@@ -704,102 +425,111 @@ def get_ordered_amount(params):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(params, for_doc):
|
||||
condition = f"expense_account = '{params.expense_account}'"
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and params.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
start_date, end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
|
||||
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
def get_actual_expense(params):
|
||||
if not params.budget_against_doctype:
|
||||
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
|
||||
def get_actual_expense(args):
|
||||
if not args.budget_against_doctype:
|
||||
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
|
||||
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||
|
||||
date_condition = (
|
||||
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
|
||||
)
|
||||
|
||||
if params.is_tree:
|
||||
if args.is_tree:
|
||||
lft_rgt = frappe.db.get_value(
|
||||
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
params.update(lft_rgt)
|
||||
|
||||
condition2 = f"""
|
||||
and exists(
|
||||
select name from `tab{params.budget_against_doctype}`
|
||||
where lft >= %(lft)s and rgt <= %(rgt)s
|
||||
and name = gle.{budget_against_field}
|
||||
)
|
||||
"""
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})"""
|
||||
else:
|
||||
condition2 = f"""
|
||||
and gle.{budget_against_field} = %({budget_against_field})s
|
||||
"""
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account = %(account)s
|
||||
{condition1}
|
||||
{date_condition}
|
||||
and gle.company = %(company)s
|
||||
and gle.docstatus = 1
|
||||
{condition2}
|
||||
""",
|
||||
params,
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account=%(account)s
|
||||
{condition1}
|
||||
and gle.fiscal_year=%(fiscal_year)s
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""",
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_accumulated_monthly_budget(budget_name, posting_date):
|
||||
posting_date = getdate(posting_date)
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
bd = frappe.qb.DocType("Budget Distribution")
|
||||
b = frappe.qb.DocType("Budget")
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(bd)
|
||||
.join(b)
|
||||
.on(bd.parent == b.name)
|
||||
.select(Sum(bd.amount).as_("accumulated_amount"))
|
||||
.where(b.name == budget_name)
|
||||
.where(bd.start_date <= posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
return flt(result[0]["accumulated_amount"]) if result else 0.0
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution 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:
|
||||
@@ -807,7 +537,7 @@ def get_item_details(params):
|
||||
|
||||
if not (cost_center and expense_account):
|
||||
for doctype in ["Item Group", "Company"]:
|
||||
data = get_expense_cost_center(doctype, params)
|
||||
data = get_expense_cost_center(doctype, args)
|
||||
|
||||
if not cost_center and data:
|
||||
cost_center = data[0]
|
||||
@@ -821,39 +551,14 @@ def get_item_details(params):
|
||||
return cost_center, expense_account
|
||||
|
||||
|
||||
def get_expense_cost_center(doctype, params):
|
||||
def get_expense_cost_center(doctype, args):
|
||||
if doctype == "Item Group":
|
||||
return frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
|
||||
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_value(
|
||||
doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
)
|
||||
|
||||
|
||||
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||
from_year = frappe.get_cached_value(
|
||||
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
to_year = frappe.get_cached_value(
|
||||
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
return from_year.year_start_date, to_year.year_end_date
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def revise_budget(budget_name):
|
||||
old_budget = frappe.get_doc("Budget", budget_name)
|
||||
|
||||
if old_budget.docstatus == 1:
|
||||
old_budget.cancel()
|
||||
|
||||
new_budget = frappe.copy_doc(old_budget)
|
||||
new_budget.docstatus = 0
|
||||
new_budget.revision_of = old_budget.name
|
||||
new_budget.insert()
|
||||
|
||||
return new_budget.name
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, 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
|
||||
@@ -25,15 +25,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
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"
|
||||
|
||||
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",
|
||||
@@ -54,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",
|
||||
@@ -78,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",
|
||||
@@ -110,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",
|
||||
@@ -154,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
|
||||
@@ -179,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",
|
||||
@@ -205,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",
|
||||
@@ -222,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"})
|
||||
|
||||
@@ -242,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
|
||||
@@ -271,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
|
||||
@@ -303,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",
|
||||
@@ -342,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",
|
||||
@@ -386,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",
|
||||
@@ -406,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",
|
||||
@@ -444,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":
|
||||
@@ -611,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:
|
||||
@@ -666,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")
|
||||
|
||||
@@ -701,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
|
||||
@@ -737,13 +530,7 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
budget.insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
if args.submit_budget:
|
||||
budget.submit()
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
return budget
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-12 23:31:03.841996",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"amount",
|
||||
"percent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "percent",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percent"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 13:18:28.398198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Distribution",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -101,11 +101,10 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:02.873815",
|
||||
"modified": "2024-03-27 13:06:47.653110",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -142,9 +141,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,13 +105,11 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
currency=company_currency,
|
||||
company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
@@ -83,11 +81,10 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 8500.0)
|
||||
self.assertEqual(je.total_credit, 8500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance")],
|
||||
fields=["sum(debit)-sum(credit) as balance"],
|
||||
)[0]
|
||||
self.assertEqual(acc_balance.balance, 8500.0)
|
||||
|
||||
@@ -149,15 +146,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 500.0)
|
||||
self.assertEqual(je.total_credit, 500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency
|
||||
@@ -199,15 +193,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account should have balance only in account currency
|
||||
@@ -244,15 +235,12 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency post revaluation
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -71,9 +71,7 @@ class PeriodValue:
|
||||
class AccountData:
|
||||
"""Account data across all periods"""
|
||||
|
||||
account: str # docname
|
||||
account_name: str = "" # account name
|
||||
account_number: str = ""
|
||||
account_name: str
|
||||
period_values: dict[str, PeriodValue] = field(default_factory=dict)
|
||||
|
||||
def add_period(self, period_value: PeriodValue) -> None:
|
||||
@@ -105,11 +103,7 @@ class AccountData:
|
||||
# movement is unaccumulated by default
|
||||
|
||||
def copy(self):
|
||||
copied = AccountData(
|
||||
account=self.account,
|
||||
account_name=self.account_name,
|
||||
account_number=self.account_number,
|
||||
)
|
||||
copied = AccountData(account_name=self.account_name)
|
||||
copied.period_values = {k: v.copy() for k, v in self.period_values.items()}
|
||||
return copied
|
||||
|
||||
@@ -335,10 +329,12 @@ class DataCollector:
|
||||
self.account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
|
||||
def add_account_request(self, row):
|
||||
accounts = self._parse_account_filter(self.company, row)
|
||||
|
||||
self.account_requests.append(
|
||||
{
|
||||
"row": row,
|
||||
"accounts": self._parse_account_filter(self.company, row),
|
||||
"accounts": accounts,
|
||||
"balance_type": row.balance_type,
|
||||
"reference_code": row.reference_code,
|
||||
"reverse_sign": row.reverse_sign,
|
||||
@@ -349,12 +345,12 @@ class DataCollector:
|
||||
if not self.account_requests:
|
||||
return {"account_data": {}, "summary": {}, "account_details": {}}
|
||||
|
||||
# Get all accounts
|
||||
all_accounts = []
|
||||
|
||||
# Get all unique accounts
|
||||
all_accounts = set()
|
||||
for request in self.account_requests:
|
||||
all_accounts.extend(request["accounts"])
|
||||
all_accounts.update(request["accounts"])
|
||||
|
||||
all_accounts = list(all_accounts)
|
||||
if not all_accounts:
|
||||
return {"account_data": {}, "summary": {}, "account_details": {}}
|
||||
|
||||
@@ -377,9 +373,7 @@ class DataCollector:
|
||||
total_values = [0.0] * len(self.periods)
|
||||
request_account_details = {}
|
||||
|
||||
for account in accounts:
|
||||
account_name = account.name
|
||||
|
||||
for account_name in accounts:
|
||||
if account_name not in account_data:
|
||||
continue
|
||||
|
||||
@@ -402,21 +396,20 @@ class DataCollector:
|
||||
return {"account_data": account_data, "summary": summary, "account_details": account_details}
|
||||
|
||||
@staticmethod
|
||||
def _parse_account_filter(company, report_row) -> list[dict]:
|
||||
def _parse_account_filter(company, report_row) -> list[str]:
|
||||
"""
|
||||
Find accounts matching filter criteria.
|
||||
|
||||
Example:
|
||||
|
||||
- Input: '["account_type", "=", "Cash"]'
|
||||
- Output: [{"name": "Cash - COMP", "account_name": "Cash", "account_number": "1001"}]
|
||||
Input: '["account_type", "=", "Cash"]'
|
||||
Output: ["Cash - COMP", "Petty Cash - COMP", "Bank - COMP"]
|
||||
"""
|
||||
filter_parser = FilterExpressionParser()
|
||||
|
||||
account = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(account)
|
||||
.select(account.name, account.account_name, account.account_number)
|
||||
.select(account.name)
|
||||
.where(account.disabled == 0)
|
||||
.where(account.is_group == 0)
|
||||
)
|
||||
@@ -430,8 +423,8 @@ class DataCollector:
|
||||
|
||||
query = query.where(where_condition)
|
||||
query = query.orderby(account.name)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
result = query.run(as_dict=True)
|
||||
return [row.name for row in result]
|
||||
|
||||
@staticmethod
|
||||
def get_filtered_accounts(company: str, account_rows: list) -> list[str]:
|
||||
@@ -463,35 +456,17 @@ class FinancialQueryBuilder:
|
||||
self.filters = filters
|
||||
self.periods = periods
|
||||
self.company = filters.get("company")
|
||||
self.account_meta = {} # {name: {account_name, account_number}}
|
||||
|
||||
def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]:
|
||||
def fetch_account_balances(self, accounts: list[str]) -> dict[str, AccountData]:
|
||||
"""
|
||||
Fetch account balances for all periods with optimization.
|
||||
Steps: get opening balances → fetch GL entries → calculate running totals
|
||||
|
||||
- accounts: list of accounts with details
|
||||
|
||||
```
|
||||
{
|
||||
"name": "Cash - COMP",
|
||||
"account_name": "Cash",
|
||||
"account_number": "1001",
|
||||
}
|
||||
```
|
||||
|
||||
Returns:
|
||||
dict: {account: AccountData}
|
||||
"""
|
||||
account_names = list({acc.name for acc in accounts})
|
||||
# NOTE: do not change accounts list as it is used in caller function
|
||||
self.account_meta = {
|
||||
acc.name: {"account_name": acc.account_name, "account_number": acc.account_number}
|
||||
for acc in accounts
|
||||
}
|
||||
|
||||
balances_data = self._get_opening_balances(account_names)
|
||||
gl_data = self._get_gl_movements(account_names)
|
||||
balances_data = self._get_opening_balances(accounts)
|
||||
gl_data = self._get_gl_movements(accounts)
|
||||
self._calculate_running_balances(balances_data, gl_data)
|
||||
self._handle_balance_accumulation(balances_data)
|
||||
|
||||
@@ -568,8 +543,7 @@ class FinancialQueryBuilder:
|
||||
gap_movement = gap_movements.get(account, 0.0)
|
||||
opening_balance = closing_balance + gap_movement
|
||||
|
||||
account_data = AccountData(account=account, **self._get_account_meta(account))
|
||||
|
||||
account_data = AccountData(account)
|
||||
account_data.add_period(PeriodValue(first_period_key, opening_balance, 0, 0))
|
||||
balances_data[account] = account_data
|
||||
|
||||
@@ -639,7 +613,7 @@ class FinancialQueryBuilder:
|
||||
for row in gl_data:
|
||||
account = row["account"]
|
||||
if account not in balances_data:
|
||||
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
||||
balances_data[account] = AccountData(account)
|
||||
|
||||
account_data: AccountData = balances_data[account]
|
||||
|
||||
@@ -740,9 +714,6 @@ class FinancialQueryBuilder:
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def _get_account_meta(self, account: str) -> dict[str, Any]:
|
||||
return self.account_meta.get(account, {})
|
||||
|
||||
|
||||
class FilterExpressionParser:
|
||||
"""Direct filter expression to SQL condition builder"""
|
||||
@@ -1573,29 +1544,20 @@ class RowFormatterBase(ABC):
|
||||
pass
|
||||
|
||||
def _get_values(self, row_data: RowData) -> dict[str, Any]:
|
||||
def _get_row_data(key: str, default: Any = "") -> Any:
|
||||
return getattr(row_data.row, key, default) or default
|
||||
|
||||
def _get_filter_value(key: str, default: Any = "") -> Any:
|
||||
return getattr(self.context.filters, key, default) or default
|
||||
|
||||
# TODO: can be commonify COA? @abdeali
|
||||
child_accounts = []
|
||||
|
||||
if row_data.account_details:
|
||||
child_accounts = list(row_data.account_details.keys())
|
||||
|
||||
display_name = _get_row_data("display_name", "")
|
||||
|
||||
values = {
|
||||
"account": _get_row_data("account", "") or display_name,
|
||||
"account_name": display_name,
|
||||
"acc_name": _get_row_data("account_name", ""),
|
||||
"acc_number": _get_row_data("account_number", ""),
|
||||
"child_accounts": child_accounts,
|
||||
"account": getattr(row_data.row, "display_name", "") or "",
|
||||
"indent": getattr(row_data.row, "indentation_level", 0),
|
||||
"account_name": getattr(row_data.row, "account", "") or "",
|
||||
"currency": self.context.currency or "",
|
||||
"indent": _get_row_data("indentation_level", 0),
|
||||
"period_start_date": _get_filter_value("period_start_date", ""),
|
||||
"period_end_date": _get_filter_value("period_end_date", ""),
|
||||
"period_start_date": getattr(self.context.filters, "period_start_date", "") or "",
|
||||
"period_end_date": getattr(self.context.filters, "period_end_date", "") or "",
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
@@ -1708,8 +1670,8 @@ class DetailRowBuilder:
|
||||
detail_rows = []
|
||||
parent_row = self.parent_row_data.row
|
||||
|
||||
for account_data in self.parent_row_data.account_details.values():
|
||||
detail_row = self._create_detail_row_object(account_data, parent_row)
|
||||
for account_name, account_data in self.parent_row_data.account_details.items():
|
||||
detail_row = self._create_detail_row_object(account_name, parent_row)
|
||||
|
||||
balance_type = getattr(parent_row, "balance_type", "Closing Balance")
|
||||
values = account_data.get_values_by_type(balance_type)
|
||||
@@ -1725,20 +1687,16 @@ class DetailRowBuilder:
|
||||
|
||||
return detail_rows
|
||||
|
||||
def _create_detail_row_object(self, account_data: AccountData, parent_row):
|
||||
acc_name = account_data.account_name or ""
|
||||
acc_number = account_data.account_number or ""
|
||||
|
||||
display_name = f"{_(acc_number)} - {_(acc_name)}" if acc_number else _(acc_name)
|
||||
def _create_detail_row_object(self, account_name: str, parent_row):
|
||||
short_name = account_name.rsplit(" - ", 1)[0].strip()
|
||||
|
||||
return type(
|
||||
"DetailRow",
|
||||
(),
|
||||
{
|
||||
"account": account_data.account,
|
||||
"display_name": display_name,
|
||||
"account_name": acc_name,
|
||||
"account_number": acc_number,
|
||||
"display_name": short_name,
|
||||
"account": account_name,
|
||||
"account_name": short_name,
|
||||
"data_source": "Account Detail",
|
||||
"indentation_level": getattr(parent_row, "indentation_level", 0) + 1,
|
||||
"fieldtype": getattr(parent_row, "fieldtype", None),
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
import shutil
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.account_category.account_category import import_account_categories
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.database.query import SQLFunctionParser
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -5,9 +5,13 @@ import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
AccountData,
|
||||
DataCollector,
|
||||
DependencyResolver,
|
||||
FilterExpressionParser,
|
||||
FinancialQueryBuilder,
|
||||
FormulaCalculator,
|
||||
PeriodValue,
|
||||
)
|
||||
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
|
||||
FinancialReportTemplateTestCase,
|
||||
|
||||
@@ -4,7 +4,6 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "fiscal_year",
|
||||
"non_standard_fieldnames": {"Budget": "from_fiscal_year"},
|
||||
"transactions": [
|
||||
{"label": _("Budgets"), "items": ["Budget"]},
|
||||
{"label": _("References"), "items": ["Period Closing Voucher"]},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -100,7 +100,7 @@ class GLEntry(Document):
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
@@ -193,6 +193,7 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -206,6 +207,7 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -274,7 +276,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
@@ -417,16 +419,16 @@ def update_outstanding_amt(
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(company, account, adv_adj=None):
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not role_allowed_for_frozen_entries:
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
@@ -440,7 +442,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.naming import parse_naming_series
|
||||
|
||||
@@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = {
|
||||
return [__("Disbursed"), "blue", "status,=,Disbursed"];
|
||||
} else if (doc.status == "Settled") {
|
||||
return [__("Settled"), "orange", "status,=,Settled"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
} else if (doc.status == "Canceled") {
|
||||
return [__("Canceled"), "red", "status,=,Canceled"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -43,20 +43,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
@@ -126,11 +112,9 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
|
||||
if (frm.doc.voucher_type !== "Exchange Gain Or Loss") {
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
}
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
},
|
||||
before_save: function (frm) {
|
||||
if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) {
|
||||
@@ -217,7 +201,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
@@ -268,10 +251,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
});
|
||||
|
||||
var update_jv_details = function (doc, r) {
|
||||
@@ -741,8 +720,6 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
erpnext.journal_entry.clear_fields(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
@@ -767,13 +744,4 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
clear_fields: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
|
||||
row.party_type = null;
|
||||
row.party = null;
|
||||
row.bank_account = null;
|
||||
|
||||
frm.refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
@@ -522,7 +517,7 @@
|
||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding "
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
@@ -591,39 +586,6 @@
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -638,7 +600,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
@@ -18,7 +17,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -32,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -49,7 +49,6 @@ class JournalEntry(AccountsController):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -66,7 +65,6 @@ class JournalEntry(AccountsController):
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
is_system_generated: DF.Check
|
||||
@@ -75,7 +73,6 @@ class JournalEntry(AccountsController):
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -87,8 +84,6 @@ class JournalEntry(AccountsController):
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
title: DF.Data | None
|
||||
total_amount: DF.Currency
|
||||
total_amount_currency: DF.Link | None
|
||||
@@ -155,8 +150,8 @@ class JournalEntry(AccountsController):
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
JournalTaxWithholding(self).on_validate()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
@@ -180,16 +175,15 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_submit")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
@@ -205,7 +199,6 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
JournalTaxWithholding(self).on_submit()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
@@ -289,8 +282,6 @@ class JournalEntry(AccountsController):
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# Cancel tax withholding entries
|
||||
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -304,10 +295,8 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
JournalTaxWithholding(self).on_cancel()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -363,6 +352,95 @@ class JournalEntry(AccountsController):
|
||||
StockAccountInvalidTransaction,
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
self.update_asset_on_depreciation()
|
||||
self.update_asset_on_disposal()
|
||||
@@ -375,7 +453,7 @@ class JournalEntry(AccountsController):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_cached_doc("Asset", d.reference_name)
|
||||
@@ -557,27 +635,12 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
@@ -1656,9 +1719,6 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
|
||||
"party_type": party_type,
|
||||
"account_type": account_details.account_type,
|
||||
"account_currency": account_details.account_currency or company_currency,
|
||||
"bank_account": (
|
||||
frappe.db.get_value("Bank Account", {"account": account, "company": company}) or None
|
||||
),
|
||||
# The date used to retreive the exchange rate here is the date passed in
|
||||
# as an argument to this function. It is assumed to be the date on which the balance is sought
|
||||
"exchange_rate": get_exchange_rate(
|
||||
@@ -1691,6 +1751,8 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1713,7 +1775,7 @@ def get_exchange_rate(
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -282,19 +281,12 @@
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
@@ -11,5 +11,6 @@ def get_data():
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
|
||||
{"items": ["Budget"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ class OpeningInvoiceCreationTool(Document):
|
||||
max_count = {}
|
||||
fields = [
|
||||
"company",
|
||||
{"COUNT": "*", "as": "total_invoices"},
|
||||
{"SUM": "outstanding_amount", "as": "outstanding_amount"},
|
||||
"count(name) as total_invoices",
|
||||
"sum(outstanding_amount) as outstanding_amount",
|
||||
]
|
||||
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
|
||||
if not companies:
|
||||
@@ -214,9 +214,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if self.invoice_type == "Purchase" and row.supplier_invoice_date:
|
||||
invoice.update({"bill_date": row.supplier_invoice_date})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
for dimension in accounting_dimension:
|
||||
invoice.update({dimension: self.get(dimension) or item.get(dimension)})
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"due_date",
|
||||
"supplier_invoice_date",
|
||||
"section_break_5",
|
||||
"item_name",
|
||||
"outstanding_amount",
|
||||
@@ -112,26 +111,19 @@
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invoice Number"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Supplier Invoice Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-01 16:18:07.997594",
|
||||
"modified": "2024-03-27 13:10:06.703006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
qty: DF.Data | None
|
||||
supplier_invoice_date: DF.Date | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -182,7 +181,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Dunning",
|
||||
];
|
||||
|
||||
if (party_type_doctypes.includes(child.reference_doctype)) {
|
||||
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||
}
|
||||
|
||||
@@ -427,15 +426,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
"party_name",
|
||||
],
|
||||
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
@@ -541,7 +532,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
() => frm.set_value("party_name", r.message.party_name),
|
||||
() => frm.clear_table("references"),
|
||||
() => frm.clear_table("tax_withholding_entries"),
|
||||
() => frm.events.hide_unhide_fields(frm),
|
||||
() => frm.events.set_dynamic_labels(frm),
|
||||
() => {
|
||||
@@ -574,15 +564,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
if (!frm.doc.apply_tds) {
|
||||
apply_tax_withholding_amount: function (frm) {
|
||||
if (!frm.doc.apply_tax_withholding_amount) {
|
||||
frm.set_value("tax_withholding_category", "");
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
|
||||
} else {
|
||||
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
|
||||
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
||||
});
|
||||
}
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
paid_from: function (frm) {
|
||||
@@ -1041,7 +1030,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
if (!frm.events.get_order_doctypes(frm).includes(d.voucher_type)) {
|
||||
if (!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
|
||||
if (flt(d.outstanding_amount) > 0)
|
||||
total_positive_outstanding += flt(d.outstanding_amount);
|
||||
else total_negative_outstanding += Math.abs(flt(d.outstanding_amount));
|
||||
@@ -1057,7 +1046,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
} else {
|
||||
c.exchange_rate = 1;
|
||||
}
|
||||
if (frm.events.get_invoice_doctypes(frm).includes(d.reference_doctype)) {
|
||||
if (in_list(frm.events.get_invoice_doctypes(frm), d.reference_doctype)) {
|
||||
c.due_date = d.due_date;
|
||||
}
|
||||
});
|
||||
@@ -1288,14 +1277,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
company_defaults?.[account_fieldname] ||
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = company_defaults?.cost_center;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
@@ -1465,6 +1455,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
$.each(frm.doc["taxes"] || [], function (i, tax) {
|
||||
frm.events.validate_taxes_and_charges(tax);
|
||||
frm.events.validate_inclusive_tax(tax);
|
||||
tax.item_wise_tax_detail = {};
|
||||
let tax_fields = [
|
||||
"total",
|
||||
"tax_fraction_for_current_item",
|
||||
@@ -1505,14 +1496,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
||||
);
|
||||
d.row_id = "";
|
||||
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
|
||||
} else if (
|
||||
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
|
||||
d.row_id
|
||||
) {
|
||||
if (d.idx == 1) {
|
||||
msg = __(
|
||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||
);
|
||||
d.charge_type = "";
|
||||
} else if (!d.row_id) {
|
||||
d.row_id = d.idx - 1;
|
||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
||||
d.row_id = "";
|
||||
} else if (d.row_id && d.row_id >= d.idx) {
|
||||
msg = __(
|
||||
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user