mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 10:49:09 +00:00
Merge branch 'develop' into version-16-beta
This commit is contained in:
@@ -45,3 +45,9 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
|||||||
|
|
||||||
# `frappe.flags.in_test` => `frappe.in_test`
|
# `frappe.flags.in_test` => `frappe.in_test`
|
||||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||||
|
|
||||||
|
# these commits actually changed something valuable
|
||||||
|
# but they have a lot of whitespace changes that make blame noisy
|
||||||
|
# PR: https://github.com/frappe/erpnext/pull/49816
|
||||||
|
3ffd50c772735877b330d010c1058f623da8721d
|
||||||
|
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6
|
||||||
|
|||||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Actions
|
- name: Checkout Actions
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: "frappe/backport"
|
repository: "frappe/backport"
|
||||||
path: ./actions
|
path: ./actions
|
||||||
|
|||||||
4
.github/workflows/docs-checker.yml
vendored
4
.github/workflows/docs-checker.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 'Setup Environment'
|
- name: 'Setup Environment'
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: 'Clone repo'
|
- name: 'Clone repo'
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Validate Docs
|
- name: Validate Docs
|
||||||
env:
|
env:
|
||||||
|
|||||||
6
.github/workflows/generate-pot-file.yml
vendored
6
.github/workflows/generate-pot-file.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.branch }}
|
ref: ${{ matrix.branch }}
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.14"
|
||||||
|
|
||||||
- name: Run script to update POT file
|
- name: Run script to update POT file
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
|||||||
name: linters
|
name: linters
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python 3.10
|
- name: Set up Python 3.14
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.14'
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
- name: Install and Run Pre-commit
|
- name: Install and Run Pre-commit
|
||||||
@@ -27,12 +27,12 @@ jobs:
|
|||||||
name: semgrep
|
name: semgrep
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python 3.10
|
- name: Set up Python 3.14
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.14'
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
- name: Download Semgrep rules
|
- name: Download Semgrep rules
|
||||||
|
|||||||
21
.github/workflows/patch.yml
vendored
21
.github/workflows/patch.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mariadb:10.6
|
image: mariadb:11.8
|
||||||
env:
|
env:
|
||||||
MARIADB_ROOT_PASSWORD: 'root'
|
MARIADB_ROOT_PASSWORD: 'root'
|
||||||
ports:
|
ports:
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for valid Python & Merge Conflicts
|
- name: Check for valid Python & Merge Conflicts
|
||||||
run: |
|
run: |
|
||||||
@@ -49,14 +49,17 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: |
|
||||||
|
3.11
|
||||||
|
3.13
|
||||||
|
3.14
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 24
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Add to Hosts
|
- name: Add to Hosts
|
||||||
@@ -132,15 +135,15 @@ jobs:
|
|||||||
# Resetup env and install apps
|
# Resetup env and install apps
|
||||||
pgrep honcho | xargs kill
|
pgrep honcho | xargs kill
|
||||||
rm -rf ~/frappe-bench/env
|
rm -rf ~/frappe-bench/env
|
||||||
bench -v setup env
|
bench -v setup env --python python$2
|
||||||
bench pip install -e ./apps/erpnext
|
bench pip install -e ./apps/erpnext
|
||||||
bench start &>> ~/frappe-bench/bench_start.log &
|
bench start &>> ~/frappe-bench/bench_start.log &
|
||||||
|
|
||||||
bench --site test_site migrate
|
bench --site test_site migrate
|
||||||
}
|
}
|
||||||
|
|
||||||
update_to_version 14
|
update_to_version 14 3.11
|
||||||
update_to_version 15
|
update_to_version 15 3.13
|
||||||
|
|
||||||
echo "Updating to latest version"
|
echo "Updating to latest version"
|
||||||
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Entire Repository
|
- name: Checkout Entire Repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: Setup dependencies
|
- 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 }}
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- id: set-matrix
|
- id: set-matrix
|
||||||
run: |
|
run: |
|
||||||
# Use grep and find to get the list of test files
|
# Use grep and find to get the list of test files
|
||||||
@@ -72,17 +72,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.14'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 24
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Add to Hosts
|
- 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
|
name: Check Commit Titles
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 200
|
fetch-depth: 200
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
17
.github/workflows/server-tests-mariadb.yml
vendored
17
.github/workflows/server-tests-mariadb.yml
vendored
@@ -62,12 +62,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.14'
|
||||||
|
|
||||||
- name: Check for valid Python & Merge Conflicts
|
- name: Check for valid Python & Merge Conflicts
|
||||||
run: |
|
run: |
|
||||||
@@ -78,9 +78,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 24
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Add to Hosts
|
- name: Add to Hosts
|
||||||
@@ -128,10 +128,9 @@ jobs:
|
|||||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||||
env:
|
env:
|
||||||
TYPE: server
|
TYPE: server
|
||||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
|
||||||
|
|
||||||
|
|
||||||
- name: Show bench output
|
- name: Show bench output
|
||||||
@@ -140,7 +139,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload coverage data
|
- name: Upload coverage data
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.container }}
|
name: coverage-${{ matrix.container }}
|
||||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||||
@@ -149,10 +147,9 @@ jobs:
|
|||||||
name: Coverage Wrap Up
|
name: Coverage Wrap Up
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
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:
|
steps:
|
||||||
|
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.14'
|
||||||
|
|
||||||
- name: Check for valid Python & Merge Conflicts
|
- name: Check for valid Python & Merge Conflicts
|
||||||
run: |
|
run: |
|
||||||
@@ -63,9 +63,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 24
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Add to Hosts
|
- name: Add to Hosts
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe.utils import (
|
|||||||
cint,
|
cint,
|
||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
|
formatdate,
|
||||||
get_first_day,
|
get_first_day,
|
||||||
get_last_day,
|
get_last_day,
|
||||||
get_link_to_form,
|
get_link_to_form,
|
||||||
@@ -318,7 +319,7 @@ def get_already_booked_amount(doc, item):
|
|||||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||||
|
|
||||||
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||||
|
|
||||||
def _book_deferred_revenue_or_expense(
|
def _book_deferred_revenue_or_expense(
|
||||||
item,
|
item,
|
||||||
|
|||||||
@@ -93,8 +93,10 @@ class Account(NestedSet):
|
|||||||
super().on_update()
|
super().on_update()
|
||||||
|
|
||||||
def onload(self):
|
def onload(self):
|
||||||
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
"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():
|
||||||
self.set_onload("can_freeze_account", True)
|
self.set_onload("can_freeze_account", True)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
@@ -303,10 +305,10 @@ class Account(NestedSet):
|
|||||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||||
return
|
return
|
||||||
|
|
||||||
frozen_accounts_modifier = frappe.get_cached_value(
|
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||||
)
|
)
|
||||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||||
throw(_("You are not authorized to set Frozen value"))
|
throw(_("You are not authorized to set Frozen value"))
|
||||||
|
|
||||||
def validate_balance_must_be_debit_or_credit(self):
|
def validate_balance_must_be_debit_or_credit(self):
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ frappe.treeview_settings["Account"] = {
|
|||||||
args: {
|
args: {
|
||||||
accounts: accounts,
|
accounts: accounts,
|
||||||
company: cur_tree.args.company,
|
company: cur_tree.args.company,
|
||||||
|
include_default_fb_balances: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,13 +73,12 @@
|
|||||||
"calculate_depr_using_total_days",
|
"calculate_depr_using_total_days",
|
||||||
"column_break_gjcc",
|
"column_break_gjcc",
|
||||||
"book_asset_depreciation_entry_automatically",
|
"book_asset_depreciation_entry_automatically",
|
||||||
|
"role_to_notify_on_depreciation_failure",
|
||||||
"closing_settings_tab",
|
"closing_settings_tab",
|
||||||
"period_closing_settings_section",
|
"period_closing_settings_section",
|
||||||
"acc_frozen_upto",
|
|
||||||
"ignore_account_closing_balance",
|
"ignore_account_closing_balance",
|
||||||
"use_legacy_controller_for_pcv",
|
"use_legacy_controller_for_pcv",
|
||||||
"column_break_25",
|
"column_break_25",
|
||||||
"frozen_accounts_modifier",
|
|
||||||
"tab_break_dpet",
|
"tab_break_dpet",
|
||||||
"show_balance_in_coa",
|
"show_balance_in_coa",
|
||||||
"banking_tab",
|
"banking_tab",
|
||||||
@@ -102,21 +101,6 @@
|
|||||||
"use_legacy_budget_controller"
|
"use_legacy_budget_controller"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
|
||||||
"fieldname": "acc_frozen_upto",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Accounts Frozen Till Date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
|
||||||
"fieldname": "frozen_accounts_modifier",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
|
||||||
"options": "Role"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Billing Address",
|
"default": "Billing Address",
|
||||||
"description": "Address used to determine Tax Category in transactions",
|
"description": "Address used to determine Tax Category in transactions",
|
||||||
@@ -658,6 +642,13 @@
|
|||||||
"fieldname": "use_legacy_controller_for_pcv",
|
"fieldname": "use_legacy_controller_for_pcv",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||||
|
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Role to Notify on Depreciation Failure",
|
||||||
|
"options": "Role"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -666,7 +657,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-20 14:06:08.870427",
|
"modified": "2025-12-03 20:42:13.238050",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||||
from erpnext.stock.utils import check_pending_reposting
|
|
||||||
|
|
||||||
|
|
||||||
class AccountsSettings(Document):
|
class AccountsSettings(Document):
|
||||||
@@ -23,7 +22,6 @@ class AccountsSettings(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
acc_frozen_upto: DF.Date | None
|
|
||||||
add_taxes_from_item_tax_template: DF.Check
|
add_taxes_from_item_tax_template: DF.Check
|
||||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||||
@@ -50,7 +48,6 @@ class AccountsSettings(Document):
|
|||||||
enable_party_matching: DF.Check
|
enable_party_matching: DF.Check
|
||||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||||
frozen_accounts_modifier: DF.Link | None
|
|
||||||
general_ledger_remarks_length: DF.Int
|
general_ledger_remarks_length: DF.Int
|
||||||
ignore_account_closing_balance: DF.Check
|
ignore_account_closing_balance: DF.Check
|
||||||
ignore_is_opening_check_for_reporting: DF.Check
|
ignore_is_opening_check_for_reporting: DF.Check
|
||||||
@@ -64,6 +61,7 @@ class AccountsSettings(Document):
|
|||||||
receivable_payable_remarks_length: DF.Int
|
receivable_payable_remarks_length: DF.Int
|
||||||
reconciliation_queue_size: DF.Int
|
reconciliation_queue_size: DF.Int
|
||||||
role_allowed_to_over_bill: DF.Link | None
|
role_allowed_to_over_bill: DF.Link | None
|
||||||
|
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||||
role_to_override_stop_action: DF.Link | None
|
role_to_override_stop_action: DF.Link | None
|
||||||
round_row_wise_tax: DF.Check
|
round_row_wise_tax: DF.Check
|
||||||
show_balance_in_coa: DF.Check
|
show_balance_in_coa: DF.Check
|
||||||
@@ -100,9 +98,6 @@ class AccountsSettings(Document):
|
|||||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||||
self.enable_payment_schedule_in_print()
|
self.enable_payment_schedule_in_print()
|
||||||
|
|
||||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
|
||||||
self.validate_pending_reposts()
|
|
||||||
|
|
||||||
if clear_cache:
|
if clear_cache:
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
@@ -129,10 +124,6 @@ class AccountsSettings(Document):
|
|||||||
validate_fields_for_doctype=False,
|
validate_fields_for_doctype=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_pending_reposts(self):
|
|
||||||
if self.acc_frozen_upto:
|
|
||||||
check_pending_reposting(self.acc_frozen_upto)
|
|
||||||
|
|
||||||
def validate_and_sync_auto_reconcile_config(self):
|
def validate_and_sync_auto_reconcile_config(self):
|
||||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||||
if (
|
if (
|
||||||
@@ -161,6 +152,5 @@ class AccountsSettings(Document):
|
|||||||
def drop_ar_sql_procedures(self):
|
def drop_ar_sql_procedures(self):
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||||
|
|
||||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
|
||||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
frappe.ui.form.on("Accounts Settings", {
|
frappe.ui.form.on("Accounts Settings", {
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
|
||||||
frm.set_df_property(
|
|
||||||
"frozen_accounts_modifier",
|
|
||||||
"label",
|
|
||||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
|
||||||
);
|
|
||||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
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(
|
||||||
|
"role_allowed_for_frozen_entries",
|
||||||
|
"label",
|
||||||
|
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"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,27 +0,0 @@
|
|||||||
# 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 AdvanceTax(Document):
|
|
||||||
# begin: auto-generated types
|
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from frappe.types import DF
|
|
||||||
|
|
||||||
account_head: DF.Link | None
|
|
||||||
allocated_amount: DF.Currency
|
|
||||||
parent: DF.Data
|
|
||||||
parentfield: DF.Data
|
|
||||||
parenttype: DF.Data
|
|
||||||
reference_detail: DF.Data | None
|
|
||||||
reference_name: DF.DynamicLink | None
|
|
||||||
reference_type: DF.Link | None
|
|
||||||
# end: auto-generated types
|
|
||||||
|
|
||||||
pass
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"description",
|
"description",
|
||||||
"included_in_paid_amount",
|
"included_in_paid_amount",
|
||||||
"set_by_item_tax_template",
|
"set_by_item_tax_template",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -25,7 +26,6 @@
|
|||||||
"net_amount",
|
"net_amount",
|
||||||
"tax_amount",
|
"tax_amount",
|
||||||
"total",
|
"total",
|
||||||
"allocated_amount",
|
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"base_tax_amount",
|
"base_tax_amount",
|
||||||
"base_net_amount",
|
"base_net_amount",
|
||||||
@@ -97,11 +97,11 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
@@ -172,12 +172,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Considered In Paid Amount"
|
"label": "Considered In Paid Amount"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "allocated_amount",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"label": "Allocated Amount",
|
|
||||||
"options": "currency"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fetch_from": "account_head.account_currency",
|
"fetch_from": "account_head.account_currency",
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
@@ -213,18 +207,26 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-11-22 19:16:22.346267",
|
"modified": "2025-12-15 06:42:18.707671",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Advance Taxes and Charges",
|
"name": "Advance Taxes and Charges",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class AdvanceTaxesandCharges(Document):
|
|||||||
|
|
||||||
account_head: DF.Link
|
account_head: DF.Link
|
||||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||||
allocated_amount: DF.Currency
|
|
||||||
base_net_amount: DF.Currency
|
base_net_amount: DF.Currency
|
||||||
base_tax_amount: DF.Currency
|
base_tax_amount: DF.Currency
|
||||||
base_total: DF.Currency
|
base_total: DF.Currency
|
||||||
@@ -28,10 +27,12 @@ class AdvanceTaxesandCharges(Document):
|
|||||||
currency: DF.Link | None
|
currency: DF.Link | None
|
||||||
description: DF.SmallText
|
description: DF.SmallText
|
||||||
included_in_paid_amount: DF.Check
|
included_in_paid_amount: DF.Check
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
net_amount: DF.Currency
|
net_amount: DF.Currency
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
project: DF.Link | None
|
||||||
rate: DF.Float
|
rate: DF.Float
|
||||||
row_id: DF.Data | None
|
row_id: DF.Data | None
|
||||||
set_by_item_tax_template: DF.Check
|
set_by_item_tax_template: DF.Check
|
||||||
|
|||||||
@@ -38,7 +38,10 @@
|
|||||||
"column_break_3czf",
|
"column_break_3czf",
|
||||||
"bank_party_name",
|
"bank_party_name",
|
||||||
"bank_party_account_number",
|
"bank_party_account_number",
|
||||||
"bank_party_iban"
|
"bank_party_iban",
|
||||||
|
"extended_bank_statement_section",
|
||||||
|
"included_fee",
|
||||||
|
"excluded_fee"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -233,12 +236,32 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_oufv",
|
"fieldname": "column_break_oufv",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "extended_bank_statement_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Extended Bank Statement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "included_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Included Fee",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||||
|
"fieldname": "excluded_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Excluded Fee",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-23 17:32:58.514807",
|
"modified": "2025-12-07 20:49:18.600757",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class BankTransaction(Document):
|
|||||||
date: DF.Date | None
|
date: DF.Date | None
|
||||||
deposit: DF.Currency
|
deposit: DF.Currency
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
|
excluded_fee: DF.Currency
|
||||||
|
included_fee: DF.Currency
|
||||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||||
party: DF.DynamicLink | None
|
party: DF.DynamicLink | None
|
||||||
party_type: DF.Link | None
|
party_type: DF.Link | None
|
||||||
@@ -45,9 +47,11 @@ class BankTransaction(Document):
|
|||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def before_validate(self):
|
def before_validate(self):
|
||||||
|
self.handle_excluded_fee()
|
||||||
self.update_allocated_amount()
|
self.update_allocated_amount()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_included_fee()
|
||||||
self.validate_duplicate_references()
|
self.validate_duplicate_references()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
|
||||||
@@ -307,6 +311,40 @@ class BankTransaction(Document):
|
|||||||
|
|
||||||
self.party_type, self.party = result
|
self.party_type, self.party = result
|
||||||
|
|
||||||
|
def validate_included_fee(self):
|
||||||
|
"""
|
||||||
|
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||||
|
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.included_fee and self.withdrawal:
|
||||||
|
if self.included_fee > self.withdrawal:
|
||||||
|
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||||
|
|
||||||
|
def handle_excluded_fee(self):
|
||||||
|
# Include the excluded fee on validate to handle all further processing the same
|
||||||
|
excluded_fee = flt(self.excluded_fee)
|
||||||
|
if excluded_fee <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||||
|
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||||
|
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||||
|
|
||||||
|
# Enforce directionality
|
||||||
|
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if flt(self.deposit) > 0:
|
||||||
|
self.deposit = flt(self.deposit) - excluded_fee
|
||||||
|
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||||
|
elif flt(self.withdrawal) >= 0:
|
||||||
|
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||||
|
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||||
|
self.excluded_fee = 0
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_doctypes_for_bank_reconciliation():
|
def get_doctypes_for_bank_reconciliation():
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 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)
|
||||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
|||||||
to: "{to_currency}",
|
to: "{to_currency}",
|
||||||
};
|
};
|
||||||
add_param(frm, r.message, params, result);
|
add_param(frm, r.message, params, result);
|
||||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||||
let result = ["rates", "{to_currency}"];
|
let result = ["rates", "{to_currency}"];
|
||||||
let params = {
|
let params = {
|
||||||
base: "{from_currency}",
|
base: "{from_currency}",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
"fieldname": "service_provider",
|
"fieldname": "service_provider",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Service Provider",
|
"label": "Service Provider",
|
||||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:47.653110",
|
"modified": "2025-11-25 13:03:41.896424",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Currency Exchange Settings",
|
"name": "Currency Exchange Settings",
|
||||||
@@ -141,8 +141,9 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
|||||||
disabled: DF.Check
|
disabled: DF.Check
|
||||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||||
url: DF.Data | None
|
url: DF.Data | None
|
||||||
use_http: DF.Check
|
use_http: DF.Check
|
||||||
# end: auto-generated types
|
# 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": "date", "value": "{transaction_date}"})
|
||||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||||
elif self.service_provider == "frankfurter.app":
|
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||||
self.set("result_key", [])
|
self.set("result_key", [])
|
||||||
self.set("req_params", [])
|
self.set("req_params", [])
|
||||||
|
|
||||||
@@ -105,11 +105,13 @@ class CurrencyExchangeSettings(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||||
if service_provider == "exchangerate.host":
|
if service_provider == "exchangerate.host":
|
||||||
api = "api.exchangerate.host/convert"
|
api = "api.exchangerate.host/convert"
|
||||||
elif service_provider == "frankfurter.app":
|
elif service_provider == "frankfurter.app":
|
||||||
api = "api.frankfurter.app/{transaction_date}"
|
api = "api.frankfurter.app/{transaction_date}"
|
||||||
|
elif service_provider == "frankfurter.dev":
|
||||||
|
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||||
|
|
||||||
protocol = "https://"
|
protocol = "https://"
|
||||||
if use_http:
|
if use_http:
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class GLEntry(Document):
|
|||||||
self.validate_account_details(adv_adj)
|
self.validate_account_details(adv_adj)
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
validate_balance_type(self.account, adv_adj)
|
validate_balance_type(self.account, adv_adj)
|
||||||
validate_frozen_account(self.account, adv_adj)
|
validate_frozen_account(self.company, self.account, adv_adj)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.voucher_type == "Journal Entry"
|
self.voucher_type == "Journal Entry"
|
||||||
@@ -276,7 +276,7 @@ class GLEntry(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_party(self):
|
def validate_party(self):
|
||||||
validate_party_frozen_disabled(self.party_type, self.party)
|
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||||
validate_account_party_type(self)
|
validate_account_party_type(self)
|
||||||
|
|
||||||
def validate_currency(self):
|
def validate_currency(self):
|
||||||
@@ -419,16 +419,16 @@ def update_outstanding_amt(
|
|||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
|
|
||||||
|
|
||||||
def validate_frozen_account(account, adv_adj=None):
|
def validate_frozen_account(company, account, adv_adj=None):
|
||||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||||
if frozen_account == "Yes" and not adv_adj:
|
if frozen_account == "Yes" and not adv_adj:
|
||||||
frozen_accounts_modifier = frappe.get_cached_value(
|
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
"Company", company, "role_allowed_for_frozen_entries"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not frozen_accounts_modifier:
|
if not role_allowed_for_frozen_entries:
|
||||||
frappe.throw(_("Account {0} is frozen").format(account))
|
frappe.throw(_("Account {0} is frozen").format(account))
|
||||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
|
|
||||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||||
erpnext.utils.set_letter_head(frm);
|
erpnext.utils.set_letter_head(frm);
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
},
|
},
|
||||||
|
|
||||||
voucher_type: function (frm) {
|
voucher_type: function (frm) {
|
||||||
@@ -251,6 +252,10 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
apply_tds: function (frm) {
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var update_jv_details = function (doc, r) {
|
var update_jv_details = function (doc, r) {
|
||||||
@@ -720,6 +725,8 @@ $.extend(erpnext.journal_entry, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
erpnext.journal_entry.clear_fields(frm, dt, dn);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set_amount_on_last_row: function (frm, dt, dn) {
|
set_amount_on_last_row: function (frm, dt, dn) {
|
||||||
@@ -744,4 +751,13 @@ $.extend(erpnext.journal_entry, {
|
|||||||
}
|
}
|
||||||
refresh_field("accounts");
|
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,6 +43,11 @@
|
|||||||
"total_amount_currency",
|
"total_amount_currency",
|
||||||
"total_amount",
|
"total_amount",
|
||||||
"total_amount_in_words",
|
"total_amount_in_words",
|
||||||
|
"section_tax_withholding_entry",
|
||||||
|
"tax_withholding_group",
|
||||||
|
"ignore_tax_withholding_threshold",
|
||||||
|
"override_tax_withholding_entries",
|
||||||
|
"tax_withholding_entries",
|
||||||
"reference",
|
"reference",
|
||||||
"clearance_date",
|
"clearance_date",
|
||||||
"remark",
|
"remark",
|
||||||
@@ -517,7 +522,7 @@
|
|||||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply Tax Withholding Amount "
|
"label": "Consider for Tax Withholding "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.docstatus",
|
"depends_on": "eval:doc.docstatus",
|
||||||
@@ -586,6 +591,39 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Party Not Required",
|
"label": "Party Not Required",
|
||||||
"no_copy": 1
|
"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",
|
"icon": "fa fa-file-text",
|
||||||
@@ -600,7 +638,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-09-29 13:05:46.982277",
|
"modified": "2025-11-13 17:54:14.542903",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
|||||||
validate_docs_for_deferred_accounting,
|
validate_docs_for_deferred_accounting,
|
||||||
validate_docs_for_voucher_types,
|
validate_docs_for_voucher_types,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||||
get_party_tax_withholding_details,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
cancel_exchange_gain_loss_journal,
|
cancel_exchange_gain_loss_journal,
|
||||||
@@ -33,6 +31,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
|||||||
get_depr_schedule,
|
get_depr_schedule,
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||||
@@ -49,6 +48,7 @@ class JournalEntry(AccountsController):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
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]
|
accounts: DF.Table[JournalEntryAccount]
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
@@ -65,6 +65,7 @@ class JournalEntry(AccountsController):
|
|||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
for_all_stock_asset_accounts: DF.Check
|
for_all_stock_asset_accounts: DF.Check
|
||||||
from_template: DF.Link | None
|
from_template: DF.Link | None
|
||||||
|
ignore_tax_withholding_threshold: DF.Check
|
||||||
inter_company_journal_entry_reference: DF.Link | None
|
inter_company_journal_entry_reference: DF.Link | None
|
||||||
is_opening: DF.Literal["No", "Yes"]
|
is_opening: DF.Literal["No", "Yes"]
|
||||||
is_system_generated: DF.Check
|
is_system_generated: DF.Check
|
||||||
@@ -73,6 +74,7 @@ class JournalEntry(AccountsController):
|
|||||||
multi_currency: DF.Check
|
multi_currency: DF.Check
|
||||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||||
party_not_required: DF.Check
|
party_not_required: DF.Check
|
||||||
|
override_tax_withholding_entries: DF.Check
|
||||||
pay_to_recd_from: DF.Data | None
|
pay_to_recd_from: DF.Data | None
|
||||||
payment_order: DF.Link | None
|
payment_order: DF.Link | None
|
||||||
periodic_entry_difference_account: DF.Link | None
|
periodic_entry_difference_account: DF.Link | None
|
||||||
@@ -84,6 +86,8 @@ class JournalEntry(AccountsController):
|
|||||||
stock_asset_account: DF.Link | None
|
stock_asset_account: DF.Link | None
|
||||||
stock_entry: DF.Link | None
|
stock_entry: DF.Link | None
|
||||||
tax_withholding_category: 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
|
title: DF.Data | None
|
||||||
total_amount: DF.Currency
|
total_amount: DF.Currency
|
||||||
total_amount_currency: DF.Link | None
|
total_amount_currency: DF.Link | None
|
||||||
@@ -150,8 +154,8 @@ class JournalEntry(AccountsController):
|
|||||||
self.validate_company_in_accounting_dimension()
|
self.validate_company_in_accounting_dimension()
|
||||||
self.validate_advance_accounts()
|
self.validate_advance_accounts()
|
||||||
|
|
||||||
if self.docstatus == 0:
|
JournalTaxWithholding(self).on_validate()
|
||||||
self.apply_tax_withholding()
|
|
||||||
if self.is_new() or not self.title:
|
if self.is_new() or not self.title:
|
||||||
self.title = self.get_title()
|
self.title = self.get_title()
|
||||||
|
|
||||||
@@ -199,6 +203,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.update_asset_value()
|
self.update_asset_value()
|
||||||
self.update_inter_company_jv()
|
self.update_inter_company_jv()
|
||||||
self.update_invoice_discounting()
|
self.update_invoice_discounting()
|
||||||
|
JournalTaxWithholding(self).on_submit()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_balance_for_periodic_accounting(self):
|
def get_balance_for_periodic_accounting(self):
|
||||||
@@ -282,6 +287,8 @@ class JournalEntry(AccountsController):
|
|||||||
self.repost_accounting_entries()
|
self.repost_accounting_entries()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
# Cancel tax withholding entries
|
||||||
|
|
||||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
self.ignore_linked_doctypes = (
|
self.ignore_linked_doctypes = (
|
||||||
@@ -295,8 +302,10 @@ class JournalEntry(AccountsController):
|
|||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Advance Payment Ledger Entry",
|
"Advance Payment Ledger Entry",
|
||||||
|
"Tax Withholding Entry",
|
||||||
)
|
)
|
||||||
self.make_gl_entries(1)
|
self.make_gl_entries(1)
|
||||||
|
JournalTaxWithholding(self).on_cancel()
|
||||||
self.unlink_advance_entry_reference()
|
self.unlink_advance_entry_reference()
|
||||||
self.unlink_asset_reference()
|
self.unlink_asset_reference()
|
||||||
self.unlink_inter_company_jv()
|
self.unlink_inter_company_jv()
|
||||||
@@ -352,95 +361,6 @@ class JournalEntry(AccountsController):
|
|||||||
StockAccountInvalidTransaction,
|
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):
|
def update_asset_value(self):
|
||||||
self.update_asset_on_depreciation()
|
self.update_asset_on_depreciation()
|
||||||
self.update_asset_on_disposal()
|
self.update_asset_on_disposal()
|
||||||
@@ -1719,6 +1639,9 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
|
|||||||
"party_type": party_type,
|
"party_type": party_type,
|
||||||
"account_type": account_details.account_type,
|
"account_type": account_details.account_type,
|
||||||
"account_currency": account_details.account_currency or company_currency,
|
"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
|
# 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
|
# as an argument to this function. It is assumed to be the date on which the balance is sought
|
||||||
"exchange_rate": get_exchange_rate(
|
"exchange_rate": get_exchange_rate(
|
||||||
@@ -1751,8 +1674,6 @@ def get_exchange_rate(
|
|||||||
credit=None,
|
credit=None,
|
||||||
exchange_rate=None,
|
exchange_rate=None,
|
||||||
):
|
):
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
|
||||||
|
|
||||||
account_details = frappe.get_cached_value(
|
account_details = frappe.get_cached_value(
|
||||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||||
)
|
)
|
||||||
@@ -1775,7 +1696,7 @@ def get_exchange_rate(
|
|||||||
# The date used to retreive the exchange rate here is the date passed
|
# The date used to retreive the exchange rate here is the date passed
|
||||||
# in as an argument to this function.
|
# in as an argument to this function.
|
||||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
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:
|
else:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"reference_detail_no",
|
"reference_detail_no",
|
||||||
"advance_voucher_type",
|
"advance_voucher_type",
|
||||||
"advance_voucher_no",
|
"advance_voucher_no",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"is_advance",
|
"is_advance",
|
||||||
"user_remark",
|
"user_remark",
|
||||||
@@ -281,12 +282,19 @@
|
|||||||
"options": "advance_voucher_type",
|
"options": "advance_voucher_type",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-27 13:48:32.805100",
|
"modified": "2025-11-27 12:23:33.157655",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
|||||||
debit_in_account_currency: DF.Currency
|
debit_in_account_currency: DF.Currency
|
||||||
exchange_rate: DF.Float
|
exchange_rate: DF.Float
|
||||||
is_advance: DF.Literal["No", "Yes"]
|
is_advance: DF.Literal["No", "Yes"]
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ 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()
|
accounting_dimension = get_accounting_dimensions()
|
||||||
for dimension in accounting_dimension:
|
for dimension in accounting_dimension:
|
||||||
invoice.update({dimension: self.get(dimension) or item.get(dimension)})
|
invoice.update({dimension: self.get(dimension) or item.get(dimension)})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"due_date",
|
"due_date",
|
||||||
|
"supplier_invoice_date",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"item_name",
|
"item_name",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
@@ -111,19 +112,26 @@
|
|||||||
"fieldname": "invoice_number",
|
"fieldname": "invoice_number",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Invoice Number"
|
"label": "Invoice Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
|
||||||
|
"fieldname": "supplier_invoice_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Supplier Invoice Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:06.703006",
|
"modified": "2025-12-01 16:18:07.997594",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool Item",
|
"name": "Opening Invoice Creation Tool Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class OpeningInvoiceCreationToolItem(Document):
|
|||||||
party_type: DF.Link | None
|
party_type: DF.Link | None
|
||||||
posting_date: DF.Date | None
|
posting_date: DF.Date | None
|
||||||
qty: DF.Data | None
|
qty: DF.Data | None
|
||||||
|
supplier_invoice_date: DF.Date | None
|
||||||
temporary_opening_account: DF.Link | None
|
temporary_opening_account: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
if (frm.is_new()) {
|
if (frm.is_new()) {
|
||||||
set_default_party_type(frm);
|
set_default_party_type(frm);
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -532,6 +533,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
() => frm.set_value("party_name", r.message.party_name),
|
() => frm.set_value("party_name", r.message.party_name),
|
||||||
() => frm.clear_table("references"),
|
() => frm.clear_table("references"),
|
||||||
|
() => frm.clear_table("tax_withholding_entries"),
|
||||||
() => frm.events.hide_unhide_fields(frm),
|
() => frm.events.hide_unhide_fields(frm),
|
||||||
() => frm.events.set_dynamic_labels(frm),
|
() => frm.events.set_dynamic_labels(frm),
|
||||||
() => {
|
() => {
|
||||||
@@ -564,14 +566,15 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
apply_tax_withholding_amount: function (frm) {
|
apply_tds: function (frm) {
|
||||||
if (!frm.doc.apply_tax_withholding_amount) {
|
if (!frm.doc.apply_tds) {
|
||||||
frm.set_value("tax_withholding_category", "");
|
frm.set_value("tax_withholding_category", "");
|
||||||
} else {
|
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||||
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
|
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
|
||||||
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
},
|
},
|
||||||
|
|
||||||
paid_from: function (frm) {
|
paid_from: function (frm) {
|
||||||
@@ -1277,15 +1280,14 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
const response = await get_company_defaults(frm.doc.company);
|
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||||
|
|
||||||
const account =
|
const account =
|
||||||
response.message?.[account_fieldname] ||
|
company_defaults?.[account_fieldname] ||
|
||||||
(await prompt_for_missing_account(frm, account_fieldname));
|
(await prompt_for_missing_account(frm, account_fieldname));
|
||||||
|
|
||||||
row = frm.add_child("deductions");
|
row = frm.add_child("deductions");
|
||||||
row.account = account;
|
row.account = account;
|
||||||
row.cost_center = response.message?.cost_center;
|
row.cost_center = company_defaults?.cost_center;
|
||||||
row.is_exchange_gain_loss = 1;
|
row.is_exchange_gain_loss = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1495,18 +1497,14 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
||||||
);
|
);
|
||||||
d.row_id = "";
|
d.row_id = "";
|
||||||
} else if (
|
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
|
||||||
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
|
|
||||||
d.row_id
|
|
||||||
) {
|
|
||||||
if (d.idx == 1) {
|
if (d.idx == 1) {
|
||||||
msg = __(
|
msg = __(
|
||||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||||
);
|
);
|
||||||
d.charge_type = "";
|
d.charge_type = "";
|
||||||
} else if (!d.row_id) {
|
} else if (!d.row_id) {
|
||||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
d.row_id = d.idx - 1;
|
||||||
d.row_id = "";
|
|
||||||
} else if (d.row_id && d.row_id >= d.idx) {
|
} else if (d.row_id && d.row_id >= d.idx) {
|
||||||
msg = __(
|
msg = __(
|
||||||
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"party_name",
|
"party_name",
|
||||||
"book_advance_payments_in_separate_party_account",
|
"book_advance_payments_in_separate_party_account",
|
||||||
"reconcile_on_advance_payment_date",
|
"reconcile_on_advance_payment_date",
|
||||||
|
"apply_tds",
|
||||||
|
"tax_withholding_category",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"bank_account",
|
"bank_account",
|
||||||
"party_bank_account",
|
"party_bank_account",
|
||||||
@@ -60,10 +62,6 @@
|
|||||||
"taxes_and_charges_section",
|
"taxes_and_charges_section",
|
||||||
"purchase_taxes_and_charges_template",
|
"purchase_taxes_and_charges_template",
|
||||||
"sales_taxes_and_charges_template",
|
"sales_taxes_and_charges_template",
|
||||||
"column_break_55",
|
|
||||||
"apply_tax_withholding_amount",
|
|
||||||
"tax_withholding_category",
|
|
||||||
"section_break_56",
|
|
||||||
"taxes",
|
"taxes",
|
||||||
"section_break_60",
|
"section_break_60",
|
||||||
"base_total_taxes_and_charges",
|
"base_total_taxes_and_charges",
|
||||||
@@ -71,6 +69,11 @@
|
|||||||
"total_taxes_and_charges",
|
"total_taxes_and_charges",
|
||||||
"deductions_or_loss_section",
|
"deductions_or_loss_section",
|
||||||
"deductions",
|
"deductions",
|
||||||
|
"section_tax_withholding_entry",
|
||||||
|
"tax_withholding_group",
|
||||||
|
"ignore_tax_withholding_threshold",
|
||||||
|
"override_tax_withholding_entries",
|
||||||
|
"tax_withholding_entries",
|
||||||
"transaction_references",
|
"transaction_references",
|
||||||
"reference_no",
|
"reference_no",
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
@@ -578,24 +581,17 @@
|
|||||||
"label": "Custom Remarks"
|
"label": "Custom Remarks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.apply_tax_withholding_amount",
|
"depends_on": "eval:doc.apply_tds",
|
||||||
"fieldname": "tax_withholding_category",
|
"fieldname": "tax_withholding_category",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Tax Withholding Category",
|
"label": "Tax Withholding Category",
|
||||||
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
|
"mandatory_depends_on": "eval:doc.apply_tds",
|
||||||
"options": "Tax Withholding Category"
|
"options": "Tax Withholding Category"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
|
||||||
"fieldname": "apply_tax_withholding_amount",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Apply Tax Withholding Amount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "taxes_and_charges_section",
|
"fieldname": "taxes_and_charges_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1,
|
||||||
"label": "Taxes and Charges"
|
"label": "Taxes and Charges"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -648,15 +644,6 @@
|
|||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_55",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_56",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_border": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||||
"fieldname": "received_amount_after_tax",
|
"fieldname": "received_amount_after_tax",
|
||||||
@@ -695,8 +682,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_60",
|
"fieldname": "section_break_60",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hide_border": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.docstatus==0",
|
"depends_on": "eval:doc.docstatus==0",
|
||||||
@@ -753,6 +739,46 @@
|
|||||||
"options": "No\nYes",
|
"options": "No\nYes",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||||
|
"fieldname": "apply_tds",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consider for Tax Withholding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_entries",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Tax Withholding Entries",
|
||||||
|
"options": "Tax Withholding Entry",
|
||||||
|
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "override_tax_withholding_entries",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Edit Tax Withholding Entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -767,7 +793,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-05-08 11:18:10.238085",
|
"modified": "2025-12-18 13:56:40.206038",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
|||||||
validate_docs_for_deferred_accounting,
|
validate_docs_for_deferred_accounting,
|
||||||
validate_docs_for_voucher_types,
|
validate_docs_for_voucher_types,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PaymentTaxWithholding
|
||||||
get_party_tax_withholding_details,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.general_ledger import (
|
from erpnext.accounts.general_ledger import (
|
||||||
make_gl_entries,
|
make_gl_entries,
|
||||||
make_reverse_gl_entries,
|
make_reverse_gl_entries,
|
||||||
@@ -80,9 +78,10 @@ class PaymentEntry(AccountsController):
|
|||||||
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
|
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
|
||||||
PaymentEntryReference,
|
PaymentEntryReference,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
apply_tax_withholding_amount: DF.Check
|
apply_tds: DF.Check
|
||||||
auto_repeat: DF.Link | None
|
auto_repeat: DF.Link | None
|
||||||
bank: DF.ReadOnly | None
|
bank: DF.ReadOnly | None
|
||||||
bank_account: DF.Link | None
|
bank_account: DF.Link | None
|
||||||
@@ -103,11 +102,13 @@ class PaymentEntry(AccountsController):
|
|||||||
custom_remarks: DF.Check
|
custom_remarks: DF.Check
|
||||||
deductions: DF.Table[PaymentEntryDeduction]
|
deductions: DF.Table[PaymentEntryDeduction]
|
||||||
difference_amount: DF.Currency
|
difference_amount: DF.Currency
|
||||||
|
ignore_tax_withholding_threshold: DF.Check
|
||||||
in_words: DF.SmallText | None
|
in_words: DF.SmallText | None
|
||||||
is_opening: DF.Literal["No", "Yes"]
|
is_opening: DF.Literal["No", "Yes"]
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
mode_of_payment: DF.Link | None
|
mode_of_payment: DF.Link | None
|
||||||
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
|
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
|
||||||
|
override_tax_withholding_entries: DF.Check
|
||||||
paid_amount: DF.Currency
|
paid_amount: DF.Currency
|
||||||
paid_amount_after_tax: DF.Currency
|
paid_amount_after_tax: DF.Currency
|
||||||
paid_from: DF.Link
|
paid_from: DF.Link
|
||||||
@@ -139,6 +140,8 @@ class PaymentEntry(AccountsController):
|
|||||||
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
||||||
target_exchange_rate: DF.Float
|
target_exchange_rate: DF.Float
|
||||||
tax_withholding_category: DF.Link | None
|
tax_withholding_category: DF.Link | None
|
||||||
|
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||||
|
tax_withholding_group: DF.Link | None
|
||||||
taxes: DF.Table[AdvanceTaxesandCharges]
|
taxes: DF.Table[AdvanceTaxesandCharges]
|
||||||
title: DF.Data | None
|
title: DF.Data | None
|
||||||
total_allocated_amount: DF.Currency
|
total_allocated_amount: DF.Currency
|
||||||
@@ -189,7 +192,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.validate_allocated_amount()
|
self.validate_allocated_amount()
|
||||||
self.validate_paid_invoices()
|
self.validate_paid_invoices()
|
||||||
self.ensure_supplier_is_not_blocked()
|
self.ensure_supplier_is_not_blocked()
|
||||||
self.set_tax_withholding()
|
PaymentTaxWithholding(self).on_validate()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_total_in_words()
|
self.set_total_in_words()
|
||||||
|
|
||||||
@@ -199,6 +202,7 @@ class PaymentEntry(AccountsController):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
frappe.throw(_("Difference Amount must be zero"))
|
frappe.throw(_("Difference Amount must be zero"))
|
||||||
|
PaymentTaxWithholding(self).on_submit()
|
||||||
self.update_payment_requests()
|
self.update_payment_requests()
|
||||||
self.update_payment_schedule()
|
self.update_payment_schedule()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
@@ -300,8 +304,10 @@ class PaymentEntry(AccountsController):
|
|||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Advance Payment Ledger Entry",
|
"Advance Payment Ledger Entry",
|
||||||
|
"Tax Withholding Entry",
|
||||||
)
|
)
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
|
PaymentTaxWithholding(self).on_cancel()
|
||||||
self.update_payment_requests(cancel=True)
|
self.update_payment_requests(cancel=True)
|
||||||
self.update_payment_schedule(cancel=1)
|
self.update_payment_schedule(cancel=1)
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
@@ -937,93 +943,6 @@ class PaymentEntry(AccountsController):
|
|||||||
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||||
self.in_words = money_in_words(amount, currency)
|
self.in_words = money_in_words(amount, currency)
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
|
||||||
if self.party_type != "Supplier":
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.apply_tax_withholding_amount:
|
|
||||||
return
|
|
||||||
|
|
||||||
net_total = self.calculate_tax_withholding_net_total()
|
|
||||||
|
|
||||||
# Adding args as purchase invoice to get TDS amount
|
|
||||||
args = frappe._dict(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"doctype": "Payment Entry",
|
|
||||||
"supplier": self.party,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"net_total": net_total,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
|
|
||||||
|
|
||||||
if not tax_withholding_details:
|
|
||||||
return
|
|
||||||
|
|
||||||
tax_withholding_details.update(
|
|
||||||
{"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)}
|
|
||||||
)
|
|
||||||
|
|
||||||
accounts = []
|
|
||||||
for d in self.taxes:
|
|
||||||
if d.account_head == tax_withholding_details.get("account_head"):
|
|
||||||
# Preserve user updated included in paid amount
|
|
||||||
if d.included_in_paid_amount:
|
|
||||||
tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
|
|
||||||
|
|
||||||
d.update(tax_withholding_details)
|
|
||||||
accounts.append(d.account_head)
|
|
||||||
|
|
||||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
|
||||||
self.append("taxes", tax_withholding_details)
|
|
||||||
|
|
||||||
to_remove = [
|
|
||||||
d
|
|
||||||
for d in self.taxes
|
|
||||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in to_remove:
|
|
||||||
self.remove(d)
|
|
||||||
|
|
||||||
def calculate_tax_withholding_net_total(self):
|
|
||||||
net_total = 0
|
|
||||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
|
||||||
|
|
||||||
for d in self.references:
|
|
||||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
|
||||||
if not tax_withholding_net_total:
|
|
||||||
continue
|
|
||||||
|
|
||||||
net_taxable_outstanding = max(
|
|
||||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
|
||||||
)
|
|
||||||
|
|
||||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
|
||||||
|
|
||||||
net_total += self.unallocated_amount
|
|
||||||
|
|
||||||
return net_total
|
|
||||||
|
|
||||||
def get_order_wise_tax_withholding_net_total(self):
|
|
||||||
if self.party_type == "Supplier":
|
|
||||||
doctype = "Purchase Order"
|
|
||||||
else:
|
|
||||||
doctype = "Sales Order"
|
|
||||||
|
|
||||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
frappe.db.get_all(
|
|
||||||
doctype,
|
|
||||||
filters={"name": ["in", docnames]},
|
|
||||||
fields=["name", "base_tax_withholding_net_total"],
|
|
||||||
as_list=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def apply_taxes(self):
|
def apply_taxes(self):
|
||||||
self.initialize_taxes()
|
self.initialize_taxes()
|
||||||
self.determine_exclusive_rate()
|
self.determine_exclusive_rate()
|
||||||
@@ -1874,7 +1793,7 @@ class PaymentEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
self.total_taxes_and_charges += current_tax_amount
|
self.total_taxes_and_charges += current_tax_amount
|
||||||
|
|
||||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
self.base_total_taxes_and_charges += current_tax_amount
|
||||||
|
|
||||||
if self.get("taxes"):
|
if self.get("taxes"):
|
||||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||||
|
|||||||
@@ -59,14 +59,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-11-05 16:07:47.307971",
|
"modified": "2025-08-13 06:52:46.130142",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Deduction",
|
"name": "Payment Entry Deduction",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 4,
|
||||||
"fieldname": "reference_name",
|
"fieldname": "reference_name",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Dynamic Link",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@@ -49,8 +49,10 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 2,
|
||||||
"fieldname": "due_date",
|
"fieldname": "due_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Due Date",
|
"label": "Due Date",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -174,7 +176,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-25 04:32:11.040025",
|
"modified": "2025-12-08 13:57:30.098239",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class PaymentLedgerEntry(Document):
|
|||||||
def on_update(self):
|
def on_update(self):
|
||||||
adv_adj = self.flags.adv_adj
|
adv_adj = self.flags.adv_adj
|
||||||
if not self.flags.from_repost:
|
if not self.flags.from_repost:
|
||||||
validate_frozen_account(self.account, adv_adj)
|
validate_frozen_account(self.company, self.account, adv_adj)
|
||||||
if not self.delinked:
|
if not self.delinked:
|
||||||
self.validate_account_details()
|
self.validate_account_details()
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
|
|||||||
@@ -334,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "HTML",
|
fieldtype: "HTML",
|
||||||
options: "<b> New Journal Entry will be posted for the difference amount </b>",
|
options: __(
|
||||||
|
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
|
||||||
|
).bold(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
primary_action: () => {
|
primary_action: () => {
|
||||||
|
|||||||
@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
|
|||||||
|
|
||||||
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||||
for inv in dr_cr_notes:
|
for inv in dr_cr_notes:
|
||||||
|
if (
|
||||||
|
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
|
||||||
|
< inv.allocated_amount
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
|
||||||
|
)
|
||||||
|
|
||||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||||
|
|
||||||
reconcile_dr_or_cr = (
|
reconcile_dr_or_cr = (
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ class PaymentRequest(Document):
|
|||||||
context = {
|
context = {
|
||||||
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
|
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
|
||||||
"payment_url": self.payment_url,
|
"payment_url": self.payment_url,
|
||||||
|
"payment_request": self,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.message:
|
if self.message:
|
||||||
@@ -539,6 +540,9 @@ def make_payment_request(**args):
|
|||||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||||
|
|
||||||
|
if args.dn and not isinstance(args.dn, str):
|
||||||
|
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||||
|
|
||||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||||
if not args.get("company"):
|
if not args.get("company"):
|
||||||
args.company = ref_doc.company
|
args.company = ref_doc.company
|
||||||
@@ -843,6 +847,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||||
|
doc_updates = {}
|
||||||
|
|
||||||
for ref in references:
|
for ref in references:
|
||||||
if not ref.payment_request:
|
if not ref.payment_request:
|
||||||
@@ -868,7 +873,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
|||||||
title=_("Invalid Allocated Amount"),
|
title=_("Invalid Allocated Amount"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# update status
|
# determine status
|
||||||
if new_outstanding_amount == payment_request["grand_total"]:
|
if new_outstanding_amount == payment_request["grand_total"]:
|
||||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||||
elif new_outstanding_amount == 0:
|
elif new_outstanding_amount == 0:
|
||||||
@@ -876,31 +881,37 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
|||||||
elif new_outstanding_amount > 0:
|
elif new_outstanding_amount > 0:
|
||||||
status = "Partially Paid"
|
status = "Partially Paid"
|
||||||
|
|
||||||
# update database
|
# prepare bulk update data
|
||||||
frappe.db.set_value(
|
doc_updates[ref.payment_request] = {
|
||||||
"Payment Request",
|
"outstanding_amount": new_outstanding_amount,
|
||||||
ref.payment_request,
|
"status": status,
|
||||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
}
|
||||||
)
|
|
||||||
|
# bulk update all payment requests
|
||||||
|
if doc_updates:
|
||||||
|
frappe.db.bulk_update("Payment Request", doc_updates)
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_message(doc):
|
def get_dummy_message(doc):
|
||||||
return frappe.render_template(
|
return """
|
||||||
"""{% if doc.contact_person -%}
|
{% if doc.contact_person -%}
|
||||||
<p>Dear {{ doc.contact_person }},</p>
|
<p>Dear {{ doc.contact_person }},</p>
|
||||||
{%- else %}<p>Hello,</p>{% endif %}
|
{%- else %}<p>Hello,</p>{% endif %}
|
||||||
|
|
||||||
<p>{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype,
|
<p>
|
||||||
doc.name, doc.get_formatted("grand_total")) }}</p>
|
{{ _("Requesting payment against {0} {1} for amount {2}").format(
|
||||||
|
doc.doctype,
|
||||||
|
doc.name,
|
||||||
|
payment_request.get_formatted("grand_total")
|
||||||
|
) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
|
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
|
||||||
|
|
||||||
<p>{{ _("If you have any questions, please get back to us.") }}</p>
|
<p>{{ _("If you have any questions, please get back to us.") }}</p>
|
||||||
|
|
||||||
<p>{{ _("Thank you for your business!") }}</p>
|
<p>{{ _("Thank you for your business!") }}</p>
|
||||||
""",
|
"""
|
||||||
dict(doc=doc, payment_url="{{ payment_url }}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ from erpnext.accounts.party import get_due_date, get_party_account
|
|||||||
from erpnext.controllers.queries import item_query as _item_query
|
from erpnext.controllers.queries import item_query as _item_query
|
||||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBundleStockValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class POSInvoice(SalesInvoice):
|
class POSInvoice(SalesInvoice):
|
||||||
@@ -395,32 +400,67 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if not d.serial_and_batch_bundle:
|
if not d.serial_and_batch_bundle:
|
||||||
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
|
if frappe.db.exists("Product Bundle", d.item_code):
|
||||||
d.item_code, d.warehouse
|
(
|
||||||
)
|
availability,
|
||||||
|
is_stock_item,
|
||||||
|
is_negative_stock_allowed,
|
||||||
|
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
|
||||||
|
|
||||||
|
else:
|
||||||
|
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
|
||||||
|
d.item_code, d.warehouse
|
||||||
|
)
|
||||||
|
|
||||||
if is_negative_stock_allowed:
|
if is_negative_stock_allowed:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_code, warehouse, _qty = (
|
if isinstance(availability, list):
|
||||||
frappe.bold(d.item_code),
|
error_msgs = []
|
||||||
frappe.bold(d.warehouse),
|
for item in availability:
|
||||||
frappe.bold(d.qty),
|
if flt(item["available"]) < flt(item["required"]):
|
||||||
)
|
error_msgs.append(
|
||||||
if is_stock_item and flt(available_stock) <= 0:
|
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
|
||||||
frappe.throw(
|
frappe.bold(item["item_code"]),
|
||||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
frappe.bold(flt(item["required"], 2)),
|
||||||
d.idx, item_code, warehouse
|
frappe.bold(flt(item["available"], 2)),
|
||||||
),
|
)
|
||||||
title=_("Item Unavailable"),
|
)
|
||||||
)
|
|
||||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
if error_msgs:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
_(
|
||||||
d.idx, item_code, warehouse
|
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
|
||||||
),
|
).format(
|
||||||
title=_("Item Unavailable"),
|
d.idx,
|
||||||
)
|
frappe.bold(d.item_code),
|
||||||
|
frappe.bold(d.warehouse),
|
||||||
|
"<br>".join(error_msgs),
|
||||||
|
),
|
||||||
|
title=_("Insufficient Stock for Product Bundle Items"),
|
||||||
|
exc=ProductBundleStockValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
|
||||||
|
if is_stock_item and flt(availability) <= 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
|
||||||
|
d.idx, item_code, warehouse
|
||||||
|
),
|
||||||
|
title=_("Item Out of Stock"),
|
||||||
|
)
|
||||||
|
elif is_stock_item and flt(availability) < flt(d.stock_qty):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
|
||||||
|
d.idx,
|
||||||
|
item_code,
|
||||||
|
warehouse,
|
||||||
|
frappe.bold(flt(availability, 2)),
|
||||||
|
frappe.bold(flt(d.stock_qty, 2)),
|
||||||
|
),
|
||||||
|
title=_("Insufficient Stock"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_is_pos_using_sales_invoice(self):
|
def validate_is_pos_using_sales_invoice(self):
|
||||||
self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
|
self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
|
||||||
@@ -858,8 +898,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
|
||||||
|
|
||||||
if frappe.db.get_value("Item", item_code, "is_stock_item"):
|
if frappe.db.get_value("Item", item_code, "is_stock_item"):
|
||||||
is_stock_item = True
|
is_stock_item = True
|
||||||
bin_qty = get_bin_qty(item_code, warehouse)
|
bin_qty = get_bin_qty(item_code, warehouse)
|
||||||
@@ -876,6 +914,26 @@ def get_stock_availability(item_code, warehouse):
|
|||||||
return 0, is_stock_item, False
|
return 0, is_stock_item, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
|
||||||
|
is_stock_item = True
|
||||||
|
bundle = frappe.get_doc("Product Bundle", item_code)
|
||||||
|
availabilities = []
|
||||||
|
for bundle_item in bundle.items:
|
||||||
|
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
|
||||||
|
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
|
||||||
|
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
|
||||||
|
available = bin_qty - reserved_qty
|
||||||
|
availabilities.append(
|
||||||
|
{
|
||||||
|
"item_code": bundle_item.item_code,
|
||||||
|
"required": bundle_item.qty * item_qty,
|
||||||
|
"available": available,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
|
||||||
|
|
||||||
|
|
||||||
def get_bundle_availability(bundle_item_code, warehouse):
|
def get_bundle_availability(bundle_item_code, warehouse):
|
||||||
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)
|
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)
|
||||||
|
|
||||||
|
|||||||
@@ -1024,6 +1024,84 @@ class TestPOSInvoice(IntegrationTestCase):
|
|||||||
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
|
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_bundle_stock_availability_validation(self):
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
|
||||||
|
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||||
|
init_user_and_profile,
|
||||||
|
)
|
||||||
|
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
init_user_and_profile()
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
# Create stock sub-items
|
||||||
|
sub_item_a = "_Test Bundle SubA"
|
||||||
|
if not frappe.db.exists("Item", sub_item_a):
|
||||||
|
create_item(
|
||||||
|
item_code=sub_item_a,
|
||||||
|
is_stock_item=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_item_b = "_Test Bundle SubB"
|
||||||
|
if not frappe.db.exists("Item", sub_item_b):
|
||||||
|
create_item(
|
||||||
|
item_code=sub_item_b,
|
||||||
|
is_stock_item=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add initial stock: SubA=5, SubB=2
|
||||||
|
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
|
||||||
|
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
|
||||||
|
|
||||||
|
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
|
||||||
|
bundle_item = "_Test Bundle"
|
||||||
|
if not frappe.db.exists("Item", bundle_item):
|
||||||
|
create_item(
|
||||||
|
item_code=bundle_item,
|
||||||
|
is_stock_item=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Product Bundle", bundle_item):
|
||||||
|
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
|
||||||
|
|
||||||
|
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
|
||||||
|
pos_inv_sufficient = create_pos_invoice(
|
||||||
|
item=bundle_item,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
warehouse=warehouse,
|
||||||
|
pos_profile=self.pos_profile.name,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
|
||||||
|
pos_inv_sufficient.insert()
|
||||||
|
pos_inv_sufficient.submit()
|
||||||
|
|
||||||
|
pos_inv_sufficient.cancel()
|
||||||
|
pos_inv_sufficient.delete()
|
||||||
|
|
||||||
|
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
|
||||||
|
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
|
||||||
|
|
||||||
|
pos_inv_insufficient = create_pos_invoice(
|
||||||
|
item=bundle_item,
|
||||||
|
qty=2,
|
||||||
|
rate=100,
|
||||||
|
warehouse=warehouse,
|
||||||
|
pos_profile=self.pos_profile.name,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
|
||||||
|
pos_inv_insufficient.save()
|
||||||
|
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
|
||||||
|
|
||||||
|
frappe.set_user("test@example.com")
|
||||||
|
|
||||||
|
|
||||||
def create_pos_invoice(**args):
|
def create_pos_invoice(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ def get_other_conditions(conditions, values, args):
|
|||||||
conditions += " and " + group_condition
|
conditions += " and " + group_condition
|
||||||
|
|
||||||
date = args.get("transaction_date") or frappe.get_value(
|
date = args.get("transaction_date") or frappe.get_value(
|
||||||
args.get("doctype"), args.get("name"), "posting_date"
|
args.get("doctype"), args.get("name"), "posting_date", ignore=True
|
||||||
)
|
)
|
||||||
if date:
|
if date:
|
||||||
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import create_item
|
|||||||
class TestProcessDeferredAccounting(IntegrationTestCase):
|
class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||||
def test_creation_of_ledger_entry_on_submit(self):
|
def test_creation_of_ledger_entry_on_submit(self):
|
||||||
"""test creation of gl entries on submission of document"""
|
"""test creation of gl entries on submission of document"""
|
||||||
change_acc_settings(acc_frozen_upto="2023-05-31", book_deferred_entries_based_on="Months")
|
change_acc_settings(acc_frozen_till_date="2023-05-31", book_deferred_entries_based_on="Months")
|
||||||
|
|
||||||
deferred_account = create_account(
|
deferred_account = create_account(
|
||||||
account_name="Deferred Revenue for Accounts Frozen",
|
account_name="Deferred Revenue for Accounts Frozen",
|
||||||
@@ -92,8 +92,10 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
|||||||
pda.cancel()
|
pda.cancel()
|
||||||
|
|
||||||
|
|
||||||
def change_acc_settings(acc_frozen_upto="", book_deferred_entries_based_on="Days"):
|
def change_acc_settings(
|
||||||
|
company="_Test Company", acc_frozen_till_date=None, book_deferred_entries_based_on="Days"
|
||||||
|
):
|
||||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||||
acc_settings.acc_frozen_upto = acc_frozen_upto
|
|
||||||
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
|
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
|
||||||
|
frappe.db.set_value("Company", company, "accounts_frozen_till_date", acc_frozen_till_date)
|
||||||
acc_settings.save()
|
acc_settings.save()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
|
|||||||
|
|
||||||
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
|
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
|
||||||
setup(doc) {
|
setup(doc) {
|
||||||
|
this.setup_accounting_dimension_triggers();
|
||||||
this.setup_posting_date_time_check();
|
this.setup_posting_date_time_check();
|
||||||
super.setup(doc);
|
super.setup(doc);
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Payment Request"),
|
__("Payment Request"),
|
||||||
function () {
|
function () {
|
||||||
@@ -223,7 +224,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
|
||||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,10 +363,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
me.apply_pricing_rule();
|
me.apply_pricing_rule();
|
||||||
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
|
me.frm.doc.apply_tds =
|
||||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
me.frm.tax_withholding_category || me.frm.tax_withholding_group ? 1 : 0;
|
||||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
me.frm.clear_table("tax_withholding_entries");
|
||||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
|
||||||
|
|
||||||
// while duplicating, don't change payment terms
|
// while duplicating, don't change payment terms
|
||||||
if (me.frm.doc.__run_link_triggers === false) {
|
if (me.frm.doc.__run_link_triggers === false) {
|
||||||
@@ -379,26 +378,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
apply_tds(frm) {
|
apply_tds(frm) {
|
||||||
var me = this;
|
var me = this;
|
||||||
me.frm.set_value("tax_withheld_vouchers", []);
|
me.frm.clear_table("tax_withholding_entries");
|
||||||
if (!me.frm.doc.apply_tds) {
|
|
||||||
me.frm.set_value("tax_withholding_category", "");
|
|
||||||
me.frm.set_df_property("tax_withholding_category", "hidden", 1);
|
|
||||||
} else {
|
|
||||||
me.frm.set_value("tax_withholding_category", me.frm.supplier_tds);
|
|
||||||
me.frm.set_df_property("tax_withholding_category", "hidden", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tax_withholding_category(frm) {
|
|
||||||
var me = this;
|
|
||||||
let filtered_taxes = (me.frm.doc.taxes || []).filter((row) => !row.is_tax_withholding_account);
|
|
||||||
me.frm.clear_table("taxes");
|
|
||||||
|
|
||||||
filtered_taxes.forEach((row) => {
|
|
||||||
me.frm.add_child("taxes", row);
|
|
||||||
});
|
|
||||||
|
|
||||||
me.frm.refresh_field("taxes");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
credit_to() {
|
credit_to() {
|
||||||
@@ -578,17 +558,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
|
||||||
var d = locals[cdt][cdn];
|
|
||||||
if (d.cost_center) {
|
|
||||||
var cl = doc.items || [];
|
|
||||||
for (var i = 0; i < cl.length; i++) {
|
|
||||||
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refresh_field("items");
|
|
||||||
};
|
|
||||||
|
|
||||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||||
return {
|
return {
|
||||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||||
@@ -702,10 +671,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
if (frm.doc.__onload && frm.doc.supplier) {
|
if (frm.doc.__onload && frm.doc.supplier) {
|
||||||
if (frm.is_new()) {
|
if (frm.is_new()) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
|
||||||
}
|
|
||||||
if (!frm.doc.__onload.supplier_tds) {
|
|
||||||
frm.set_df_property("apply_tds", "read_only", 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +680,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (frm.is_new()) {
|
if (frm.is_new()) {
|
||||||
frm.clear_table("tax_withheld_vouchers");
|
frm.clear_table("tax_withholding_entries");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -741,6 +707,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
|
|
||||||
company: function (frm) {
|
company: function (frm) {
|
||||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
|
|
||||||
if (frm.doc.company) {
|
if (frm.doc.company) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"update_billed_amount_in_purchase_order",
|
"update_billed_amount_in_purchase_order",
|
||||||
"update_billed_amount_in_purchase_receipt",
|
"update_billed_amount_in_purchase_receipt",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
"tax_withholding_category",
|
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"supplier_invoice_details",
|
"supplier_invoice_details",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
@@ -68,8 +67,6 @@
|
|||||||
"column_break_28",
|
"column_break_28",
|
||||||
"total",
|
"total",
|
||||||
"net_total",
|
"net_total",
|
||||||
"tax_withholding_net_total",
|
|
||||||
"base_tax_withholding_net_total",
|
|
||||||
"taxes_section",
|
"taxes_section",
|
||||||
"tax_category",
|
"tax_category",
|
||||||
"taxes_and_charges",
|
"taxes_and_charges",
|
||||||
@@ -102,14 +99,17 @@
|
|||||||
"total_advance",
|
"total_advance",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
"disable_rounded_total",
|
"disable_rounded_total",
|
||||||
|
"section_tax_withholding_entry",
|
||||||
|
"tax_withholding_group",
|
||||||
|
"ignore_tax_withholding_threshold",
|
||||||
|
"override_tax_withholding_entries",
|
||||||
|
"tax_withholding_entries",
|
||||||
"section_break_44",
|
"section_break_44",
|
||||||
"apply_discount_on",
|
"apply_discount_on",
|
||||||
"base_discount_amount",
|
"base_discount_amount",
|
||||||
"column_break_46",
|
"column_break_46",
|
||||||
"additional_discount_percentage",
|
"additional_discount_percentage",
|
||||||
"discount_amount",
|
"discount_amount",
|
||||||
"tax_withheld_vouchers_section",
|
|
||||||
"tax_withheld_vouchers",
|
|
||||||
"sec_tax_breakup",
|
"sec_tax_breakup",
|
||||||
"other_charges_calculation",
|
"other_charges_calculation",
|
||||||
"item_wise_tax_details",
|
"item_wise_tax_details",
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
"only_include_allocated_payments",
|
"only_include_allocated_payments",
|
||||||
"get_advances",
|
"get_advances",
|
||||||
"advances",
|
"advances",
|
||||||
"advance_tax",
|
|
||||||
"write_off",
|
"write_off",
|
||||||
"write_off_amount",
|
"write_off_amount",
|
||||||
"base_write_off_amount",
|
"base_write_off_amount",
|
||||||
@@ -286,7 +285,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply Tax Withholding Amount",
|
"label": "Consider for Tax Withholding",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1358,14 +1357,6 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "tax_withholding_category",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Tax Withholding Category",
|
|
||||||
"options": "Tax Withholding Category",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "billing_address",
|
"fieldname": "billing_address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -1455,14 +1446,6 @@
|
|||||||
"fieldname": "column_break_147",
|
"fieldname": "column_break_147",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "advance_tax",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Advance Tax",
|
|
||||||
"options": "Advance Tax",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "subscription",
|
"fieldname": "subscription",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -1477,42 +1460,6 @@
|
|||||||
"label": "Is Old Subcontracting Flow",
|
"label": "Is Old Subcontracting Flow",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "apply_tds",
|
|
||||||
"fieldname": "tax_withholding_net_total",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Tax Withholding Net Total",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "currency",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "apply_tds",
|
|
||||||
"fieldname": "base_tax_withholding_net_total",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Base Tax Withholding Net Total",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible_depends_on": "tax_withheld_vouchers",
|
|
||||||
"fieldname": "tax_withheld_vouchers_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Tax Withheld Vouchers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "tax_withheld_vouchers",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Tax Withheld Vouchers",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Tax Withheld Vouchers",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "payments_tab",
|
"fieldname": "payments_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
@@ -1662,7 +1609,7 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"label": "Last Scanned Warehouse"
|
"label": "Last Scanned Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "claimed_landed_cost_amount",
|
"fieldname": "claimed_landed_cost_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
@@ -1679,6 +1626,40 @@
|
|||||||
"label": "Item Wise Tax Details",
|
"label": "Item Wise Tax Details",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Item Wise Tax Detail"
|
"options": "Item Wise Tax Detail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_entries",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Tax Withholding Entries",
|
||||||
|
"options": "Tax Withholding Entry",
|
||||||
|
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1686,7 +1667,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-04 19:19:11.380664",
|
"modified": "2025-12-15 06:41:38.237728",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
|||||||
update_linked_doc,
|
update_linked_doc,
|
||||||
validate_inter_company_party,
|
validate_inter_company_party,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PurchaseTaxWithholding
|
||||||
get_party_tax_withholding_details,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.general_ledger import (
|
from erpnext.accounts.general_ledger import (
|
||||||
get_round_off_account_and_cost_center,
|
get_round_off_account_and_cost_center,
|
||||||
make_gl_entries,
|
make_gl_entries,
|
||||||
@@ -61,7 +59,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax
|
|
||||||
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
|
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
|
||||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||||
@@ -72,14 +69,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
||||||
PurchaseTaxesandCharges,
|
PurchaseTaxesandCharges,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||||
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
||||||
PurchaseReceiptItemSupplied,
|
PurchaseReceiptItemSupplied,
|
||||||
)
|
)
|
||||||
|
|
||||||
additional_discount_percentage: DF.Float
|
additional_discount_percentage: DF.Float
|
||||||
address_display: DF.TextEditor | None
|
address_display: DF.TextEditor | None
|
||||||
advance_tax: DF.Table[AdvanceTax]
|
|
||||||
advances: DF.Table[PurchaseInvoiceAdvance]
|
advances: DF.Table[PurchaseInvoiceAdvance]
|
||||||
against_expense_account: DF.SmallText | None
|
against_expense_account: DF.SmallText | None
|
||||||
allocate_advances_automatically: DF.Check
|
allocate_advances_automatically: DF.Check
|
||||||
@@ -94,7 +90,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
base_paid_amount: DF.Currency
|
base_paid_amount: DF.Currency
|
||||||
base_rounded_total: DF.Currency
|
base_rounded_total: DF.Currency
|
||||||
base_rounding_adjustment: DF.Currency
|
base_rounding_adjustment: DF.Currency
|
||||||
base_tax_withholding_net_total: DF.Currency
|
|
||||||
base_taxes_and_charges_added: DF.Currency
|
base_taxes_and_charges_added: DF.Currency
|
||||||
base_taxes_and_charges_deducted: DF.Currency
|
base_taxes_and_charges_deducted: DF.Currency
|
||||||
base_total: DF.Currency
|
base_total: DF.Currency
|
||||||
@@ -128,6 +123,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
hold_comment: DF.SmallText | None
|
hold_comment: DF.SmallText | None
|
||||||
ignore_default_payment_terms_template: DF.Check
|
ignore_default_payment_terms_template: DF.Check
|
||||||
ignore_pricing_rule: DF.Check
|
ignore_pricing_rule: DF.Check
|
||||||
|
ignore_tax_withholding_threshold: DF.Check
|
||||||
in_words: DF.Data | None
|
in_words: DF.Data | None
|
||||||
incoterm: DF.Link | None
|
incoterm: DF.Link | None
|
||||||
inter_company_invoice_reference: DF.Link | None
|
inter_company_invoice_reference: DF.Link | None
|
||||||
@@ -149,6 +145,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
only_include_allocated_payments: DF.Check
|
only_include_allocated_payments: DF.Check
|
||||||
other_charges_calculation: DF.TextEditor | None
|
other_charges_calculation: DF.TextEditor | None
|
||||||
outstanding_amount: DF.Currency
|
outstanding_amount: DF.Currency
|
||||||
|
override_tax_withholding_entries: DF.Check
|
||||||
paid_amount: DF.Currency
|
paid_amount: DF.Currency
|
||||||
party_account_currency: DF.Link | None
|
party_account_currency: DF.Link | None
|
||||||
payment_schedule: DF.Table[PaymentSchedule]
|
payment_schedule: DF.Table[PaymentSchedule]
|
||||||
@@ -198,9 +195,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
supplier_warehouse: DF.Link | None
|
supplier_warehouse: DF.Link | None
|
||||||
tax_category: DF.Link | None
|
tax_category: DF.Link | None
|
||||||
tax_id: DF.ReadOnly | None
|
tax_id: DF.ReadOnly | None
|
||||||
tax_withheld_vouchers: DF.Table[TaxWithheldVouchers]
|
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||||
tax_withholding_category: DF.Link | None
|
tax_withholding_group: DF.Link | None
|
||||||
tax_withholding_net_total: DF.Currency
|
|
||||||
taxes: DF.Table[PurchaseTaxesandCharges]
|
taxes: DF.Table[PurchaseTaxesandCharges]
|
||||||
taxes_and_charges: DF.Link | None
|
taxes_and_charges: DF.Link | None
|
||||||
taxes_and_charges_added: DF.Currency
|
taxes_and_charges_added: DF.Currency
|
||||||
@@ -245,11 +241,14 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
def onload(self):
|
def onload(self):
|
||||||
super().onload()
|
super().onload()
|
||||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
if self.supplier:
|
||||||
self.set_onload("supplier_tds", supplier_tds)
|
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||||
|
"Supplier", self.supplier, ["tax_withholding_category", "tax_withholding_group"]
|
||||||
|
)
|
||||||
|
self.set_onload("apply_tds", tax_withholding_category or tax_withholding_group)
|
||||||
|
|
||||||
if self.is_new():
|
if self.is_new():
|
||||||
self.set("tax_withheld_vouchers", [])
|
self.set("tax_withholding_entries", [])
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
if not self.on_hold:
|
if not self.on_hold:
|
||||||
@@ -300,6 +299,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||||
|
PurchaseTaxWithholding(self).on_validate()
|
||||||
self.set_percentage_received()
|
self.set_percentage_received()
|
||||||
|
|
||||||
def set_percentage_received(self):
|
def set_percentage_received(self):
|
||||||
@@ -352,11 +352,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
template_name=self.payment_terms_template,
|
template_name=self.payment_terms_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
if self.supplier:
|
||||||
if tds_category and not for_validate:
|
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||||
self.apply_tds = 1
|
"Supplier", self.supplier, ["tax_withholding_category", "tax_withholding_group"]
|
||||||
self.tax_withholding_category = tds_category
|
)
|
||||||
self.set_onload("supplier_tds", tds_category)
|
if not for_validate:
|
||||||
|
if tax_withholding_category or tax_withholding_group:
|
||||||
|
self.apply_tds = 1
|
||||||
|
|
||||||
super().set_missing_values(for_validate)
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
@@ -747,6 +749,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
super().on_submit()
|
super().on_submit()
|
||||||
|
PurchaseTaxWithholding(self).on_submit()
|
||||||
|
|
||||||
self.check_prev_docstatus()
|
self.check_prev_docstatus()
|
||||||
|
|
||||||
@@ -788,7 +791,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.update_project()
|
self.update_project()
|
||||||
|
|
||||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||||
self.update_advance_tax_references()
|
|
||||||
|
|
||||||
self.process_common_party_accounting()
|
self.process_common_party_accounting()
|
||||||
|
|
||||||
@@ -1672,6 +1674,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
check_if_return_invoice_linked_with_payment_entry(self)
|
check_if_return_invoice_linked_with_payment_entry(self)
|
||||||
|
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
|
PurchaseTaxWithholding(self).on_cancel()
|
||||||
|
|
||||||
self.check_on_hold_or_closed_status()
|
self.check_on_hold_or_closed_status()
|
||||||
|
|
||||||
@@ -1718,10 +1721,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Tax Withheld Vouchers",
|
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
|
"Tax Withholding Entry",
|
||||||
)
|
)
|
||||||
self.update_advance_tax_references(cancel=1)
|
|
||||||
|
|
||||||
def update_project(self):
|
def update_project(self):
|
||||||
projects = frappe._dict()
|
projects = frappe._dict()
|
||||||
@@ -1844,102 +1846,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.db_set("on_hold", 0)
|
self.db_set("on_hold", 0)
|
||||||
self.db_set("release_date", None)
|
self.db_set("release_date", None)
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
|
||||||
self.set("advance_tax", [])
|
|
||||||
self.set("tax_withheld_vouchers", [])
|
|
||||||
|
|
||||||
if not self.apply_tds:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.apply_tds and not self.get("tax_withholding_category"):
|
|
||||||
self.tax_withholding_category = frappe.db.get_value(
|
|
||||||
"Supplier", self.supplier, "tax_withholding_category"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.tax_withholding_category:
|
|
||||||
return
|
|
||||||
|
|
||||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
|
||||||
self, self.tax_withholding_category
|
|
||||||
)
|
|
||||||
|
|
||||||
# Adjust TDS paid on advances
|
|
||||||
self.allocate_advance_tds(tax_withholding_details, advance_taxes)
|
|
||||||
|
|
||||||
if not tax_withholding_details:
|
|
||||||
return
|
|
||||||
|
|
||||||
accounts = []
|
|
||||||
for d in self.taxes:
|
|
||||||
if d.account_head == tax_withholding_details.get("account_head"):
|
|
||||||
d.update(tax_withholding_details)
|
|
||||||
|
|
||||||
accounts.append(d.account_head)
|
|
||||||
|
|
||||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
|
||||||
self.append("taxes", tax_withholding_details)
|
|
||||||
|
|
||||||
to_remove = [
|
|
||||||
d
|
|
||||||
for d in self.taxes
|
|
||||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in to_remove:
|
|
||||||
self.remove(d)
|
|
||||||
|
|
||||||
## Add pending vouchers on which tax was withheld
|
|
||||||
for row in voucher_wise_amount:
|
|
||||||
self.append(
|
|
||||||
"tax_withheld_vouchers",
|
|
||||||
{
|
|
||||||
"voucher_name": row.voucher_name,
|
|
||||||
"voucher_type": row.voucher_type,
|
|
||||||
"taxable_amount": row.taxable_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# calculate totals again after applying TDS
|
|
||||||
self.calculate_taxes_and_totals()
|
|
||||||
|
|
||||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
|
||||||
for tax in advance_taxes:
|
|
||||||
allocated_amount = 0
|
|
||||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
|
||||||
if flt(tax_withholding_details.get("tax_amount")) >= pending_amount:
|
|
||||||
tax_withholding_details["tax_amount"] -= pending_amount
|
|
||||||
allocated_amount = pending_amount
|
|
||||||
elif (
|
|
||||||
flt(tax_withholding_details.get("tax_amount"))
|
|
||||||
and flt(tax_withholding_details.get("tax_amount")) < pending_amount
|
|
||||||
):
|
|
||||||
allocated_amount = tax_withholding_details["tax_amount"]
|
|
||||||
tax_withholding_details["tax_amount"] = 0
|
|
||||||
|
|
||||||
self.append(
|
|
||||||
"advance_tax",
|
|
||||||
{
|
|
||||||
"reference_type": "Payment Entry",
|
|
||||||
"reference_name": tax.parent,
|
|
||||||
"reference_detail": tax.name,
|
|
||||||
"account_head": tax.account_head,
|
|
||||||
"allocated_amount": allocated_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_advance_tax_references(self, cancel=0):
|
|
||||||
for tax in self.get("advance_tax"):
|
|
||||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
|
||||||
|
|
||||||
if cancel:
|
|
||||||
frappe.qb.update(at).set(
|
|
||||||
at.allocated_amount, at.allocated_amount - tax.allocated_amount
|
|
||||||
).where(at.name == tax.reference_detail).run()
|
|
||||||
else:
|
|
||||||
frappe.qb.update(at).set(
|
|
||||||
at.allocated_amount, at.allocated_amount + tax.allocated_amount
|
|
||||||
).where(at.name == tax.reference_detail).run()
|
|
||||||
|
|
||||||
def set_status(self, update=False, status=None, update_modified=True):
|
def set_status(self, update=False, status=None, update_modified=True):
|
||||||
if self.is_new():
|
if self.is_new():
|
||||||
if self.get("amended_from"):
|
if self.get("amended_from"):
|
||||||
@@ -2027,6 +1933,7 @@ def get_list_context(context=None):
|
|||||||
"show_search": True,
|
"show_search": True,
|
||||||
"no_breadcrumbs": True,
|
"no_breadcrumbs": True,
|
||||||
"title": _("Purchase Invoices"),
|
"title": _("Purchase Invoices"),
|
||||||
|
"list_template": "templates/includes/list/list.html",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return list_context
|
return list_context
|
||||||
@@ -2097,10 +2004,26 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
|||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
|
def post_parent_process(source_parent, target_parent):
|
||||||
|
for row in target_parent.get("items"):
|
||||||
|
if row.get("qty") == 0:
|
||||||
|
target_parent.remove(row)
|
||||||
|
|
||||||
def update_item(obj, target, source_parent):
|
def update_item(obj, target, source_parent):
|
||||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||||
|
|
||||||
|
returned_qty_map = (
|
||||||
|
get_returned_qty_map_for_row(
|
||||||
|
source_parent.name, source_parent.supplier, obj.name, "Purchase Invoice"
|
||||||
|
)
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
|
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||||
|
obj.conversion_factor
|
||||||
|
)
|
||||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||||
target.base_amount = (
|
target.base_amount = (
|
||||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||||
@@ -2139,6 +2062,7 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
|||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
post_parent_process,
|
||||||
)
|
)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
@@ -1538,7 +1538,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
# Create Payment Entry Against the order
|
# Create Payment Entry Against the order
|
||||||
payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
|
payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
|
||||||
payment_entry.paid_from = "Cash - _TC"
|
payment_entry.paid_from = "Cash - _TC"
|
||||||
payment_entry.apply_tax_withholding_amount = 1
|
payment_entry.apply_tds = 1
|
||||||
payment_entry.tax_withholding_category = tax_withholding_category
|
payment_entry.tax_withholding_category = tax_withholding_category
|
||||||
payment_entry.save()
|
payment_entry.save()
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
@@ -1591,12 +1591,26 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
self.assertEqual(expected_gle[i][1], gle.amount)
|
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||||
|
|
||||||
payment_entry.load_from_db()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
|
tax_allocated = sum(
|
||||||
|
[
|
||||||
|
entry.withholding_amount
|
||||||
|
for entry in payment_entry.get("tax_withholding_entries", [])
|
||||||
|
if entry.taxable_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(tax_allocated, 3000)
|
||||||
|
|
||||||
purchase_invoice.cancel()
|
purchase_invoice.cancel()
|
||||||
|
|
||||||
payment_entry.load_from_db()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
tax_allocated = sum(
|
||||||
|
[
|
||||||
|
entry.withholding_amount
|
||||||
|
for entry in payment_entry.get("tax_withholding_entries", [])
|
||||||
|
if entry.taxable_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(tax_allocated, 0)
|
||||||
|
|
||||||
def test_purchase_gl_with_tax_withholding_tax(self):
|
def test_purchase_gl_with_tax_withholding_tax(self):
|
||||||
company = "_Test Company"
|
company = "_Test Company"
|
||||||
@@ -1631,7 +1645,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
do_not_submit=1,
|
do_not_submit=1,
|
||||||
)
|
)
|
||||||
pi.apply_tds = 1
|
pi.apply_tds = 1
|
||||||
pi.tax_withholding_category = tax_withholding_category
|
|
||||||
pi.save()
|
pi.save()
|
||||||
pi.submit()
|
pi.submit()
|
||||||
|
|
||||||
@@ -2931,6 +2944,29 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
pi.save()
|
pi.save()
|
||||||
self.assertEqual(pi.discount_amount, discount_amount)
|
self.assertEqual(pi.discount_amount, discount_amount)
|
||||||
|
|
||||||
|
def test_returned_item_purchase_receipt(self):
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||||
|
make_purchase_receipt as make_purchase_receipt_from_pi,
|
||||||
|
)
|
||||||
|
|
||||||
|
item = create_item("_Test Returned Item Purchase Receipt", is_stock_item=1)
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(item_code=item.name, qty=5, rate=100)
|
||||||
|
|
||||||
|
return_pi = make_purchase_invoice(
|
||||||
|
item_code=item.name,
|
||||||
|
is_return=1,
|
||||||
|
return_against=pi.name,
|
||||||
|
qty=-5,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return_pi.items[0].purchase_invoice_item = pi.items[0].name
|
||||||
|
return_pi.submit()
|
||||||
|
|
||||||
|
pr = make_purchase_receipt_from_pi(pi.name)
|
||||||
|
self.assertFalse(pr.items)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"rate",
|
"rate",
|
||||||
"amount",
|
"amount",
|
||||||
"item_tax_template",
|
"item_tax_template",
|
||||||
|
"tax_withholding_category",
|
||||||
"col_break4",
|
"col_break4",
|
||||||
"base_rate",
|
"base_rate",
|
||||||
"base_amount",
|
"base_amount",
|
||||||
@@ -228,7 +229,6 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
|
||||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||||
"fieldname": "conversion_factor",
|
"fieldname": "conversion_factor",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@@ -893,7 +893,7 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply TDS"
|
"label": "Consider for Tax Withholding"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||||
@@ -979,13 +979,20 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_category",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Tax Withholding Category",
|
||||||
|
"options": "Tax Withholding Category",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-14 13:00:54.441511",
|
"modified": "2025-12-13 14:10:02.379392",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class PurchaseInvoiceItem(Document):
|
|||||||
stock_qty: DF.Float
|
stock_qty: DF.Float
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
stock_uom_rate: DF.Currency
|
stock_uom_rate: DF.Currency
|
||||||
|
tax_withholding_category: DF.Link | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
use_serial_batch_fields: DF.Check
|
use_serial_batch_fields: DF.Check
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"base_net_amount",
|
"base_net_amount",
|
||||||
"base_tax_amount",
|
"base_tax_amount",
|
||||||
"base_total",
|
"base_total",
|
||||||
"base_tax_amount_after_discount_amount"
|
"base_tax_amount_after_discount_amount",
|
||||||
|
"dont_recompute_tax"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -205,11 +206,11 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -262,13 +263,22 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "dont_recompute_tax",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Don't Recompute Tax",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-24 15:08:44.433022",
|
"modified": "2025-11-24 18:22:56.886010",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges",
|
"name": "Purchase Taxes and Charges",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
]
|
]
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
description: DF.SmallText
|
description: DF.SmallText
|
||||||
|
dont_recompute_tax: DF.Check
|
||||||
included_in_paid_amount: DF.Check
|
included_in_paid_amount: DF.Check
|
||||||
included_in_print_rate: DF.Check
|
included_in_print_rate: DF.Check
|
||||||
is_tax_withholding_account: DF.Check
|
is_tax_withholding_account: DF.Check
|
||||||
@@ -39,6 +40,7 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
project: DF.Link | None
|
||||||
rate: DF.Float
|
rate: DF.Float
|
||||||
row_id: DF.Data | None
|
row_id: DF.Data | None
|
||||||
set_by_item_tax_template: DF.Check
|
set_by_item_tax_template: DF.Check
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
erpnext.selling.SellingController
|
erpnext.selling.SellingController
|
||||||
) {
|
) {
|
||||||
setup(doc) {
|
setup(doc) {
|
||||||
|
this.setup_accounting_dimension_triggers();
|
||||||
this.setup_posting_date_time_check();
|
this.setup_posting_date_time_check();
|
||||||
super.setup(doc);
|
super.setup(doc);
|
||||||
this.frm.make_methods = {
|
this.frm.make_methods = {
|
||||||
@@ -24,6 +25,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
company() {
|
company() {
|
||||||
super.company();
|
super.company();
|
||||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||||
|
this.frm.clear_table("tax_withholding_entries");
|
||||||
}
|
}
|
||||||
onload() {
|
onload() {
|
||||||
var me = this;
|
var me = this;
|
||||||
@@ -381,6 +383,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
|
me.frm.doc.apply_tds =
|
||||||
|
me.frm.tax_withholding_category || me.frm.tax_withholding_group ? 1 : 0;
|
||||||
|
me.frm.clear_table("tax_withholding_entries");
|
||||||
me.apply_pricing_rule();
|
me.apply_pricing_rule();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -597,6 +602,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
|
|
||||||
this.calculate_taxes_and_totals();
|
this.calculate_taxes_and_totals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply_tds(frm) {
|
||||||
|
this.frm.clear_table("tax_withholding_entries");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// for backward compatibility: combine new and previous states
|
||||||
@@ -617,10 +626,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
|
|||||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
|
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
|
||||||
};
|
};
|
||||||
|
|
||||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
|
||||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
|
|
||||||
};
|
|
||||||
|
|
||||||
frappe.ui.form.on("Sales Invoice", {
|
frappe.ui.form.on("Sales Invoice", {
|
||||||
setup: function (frm) {
|
setup: function (frm) {
|
||||||
frm.add_fetch("customer", "tax_id", "tax_id");
|
frm.add_fetch("customer", "tax_id", "tax_id");
|
||||||
@@ -817,6 +822,16 @@ frappe.ui.form.on("Sales Invoice", {
|
|||||||
},
|
},
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
frm.redemption_conversion_factor = null;
|
frm.redemption_conversion_factor = null;
|
||||||
|
|
||||||
|
if (frm.doc.__onload && frm.doc.customer) {
|
||||||
|
if (frm.is_new()) {
|
||||||
|
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.is_new()) {
|
||||||
|
frm.clear_table("tax_withholding_entries");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
update_stock: function (frm, dt, dn) {
|
update_stock: function (frm, dt, dn) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"update_billed_amount_in_sales_order",
|
"update_billed_amount_in_sales_order",
|
||||||
"update_billed_amount_in_delivery_note",
|
"update_billed_amount_in_delivery_note",
|
||||||
"is_debit_note",
|
"is_debit_note",
|
||||||
|
"apply_tds",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"is_created_using_pos",
|
"is_created_using_pos",
|
||||||
"pos_closing_entry",
|
"pos_closing_entry",
|
||||||
@@ -90,6 +91,11 @@
|
|||||||
"total_advance",
|
"total_advance",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
"disable_rounded_total",
|
"disable_rounded_total",
|
||||||
|
"section_tax_withholding_entry",
|
||||||
|
"tax_withholding_group",
|
||||||
|
"ignore_tax_withholding_threshold",
|
||||||
|
"override_tax_withholding_entries",
|
||||||
|
"tax_withholding_entries",
|
||||||
"section_break_49",
|
"section_break_49",
|
||||||
"apply_discount_on",
|
"apply_discount_on",
|
||||||
"base_discount_amount",
|
"base_discount_amount",
|
||||||
@@ -2247,6 +2253,46 @@
|
|||||||
"label": "Item Wise Tax Details",
|
"label": "Item Wise Tax Details",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Item Wise Tax Detail"
|
"options": "Item Wise Tax Detail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "apply_tds",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consider for Tax Withholding",
|
||||||
|
"print_hide": 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_entries",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Tax Withholding Entries",
|
||||||
|
"options": "Tax Withholding Entry",
|
||||||
|
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
|||||||
validate_docs_for_deferred_accounting,
|
validate_docs_for_deferred_accounting,
|
||||||
validate_docs_for_voucher_types,
|
validate_docs_for_voucher_types,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
|
||||||
get_party_tax_withholding_details,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
@@ -77,6 +75,7 @@ class SalesInvoice(SellingController):
|
|||||||
from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import (
|
from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import (
|
||||||
SalesTaxesandCharges,
|
SalesTaxesandCharges,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||||
from erpnext.selling.doctype.sales_team.sales_team import SalesTeam
|
from erpnext.selling.doctype.sales_team.sales_team import SalesTeam
|
||||||
from erpnext.stock.doctype.packed_item.packed_item import PackedItem
|
from erpnext.stock.doctype.packed_item.packed_item import PackedItem
|
||||||
|
|
||||||
@@ -90,6 +89,7 @@ class SalesInvoice(SellingController):
|
|||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
amount_eligible_for_commission: DF.Currency
|
amount_eligible_for_commission: DF.Currency
|
||||||
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
|
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
|
||||||
|
apply_tds: DF.Check
|
||||||
auto_repeat: DF.Link | None
|
auto_repeat: DF.Link | None
|
||||||
base_change_amount: DF.Currency
|
base_change_amount: DF.Currency
|
||||||
base_discount_amount: DF.Currency
|
base_discount_amount: DF.Currency
|
||||||
@@ -135,6 +135,7 @@ class SalesInvoice(SellingController):
|
|||||||
has_subcontracted: DF.Check
|
has_subcontracted: DF.Check
|
||||||
ignore_default_payment_terms_template: DF.Check
|
ignore_default_payment_terms_template: DF.Check
|
||||||
ignore_pricing_rule: DF.Check
|
ignore_pricing_rule: DF.Check
|
||||||
|
ignore_tax_withholding_threshold: DF.Check
|
||||||
in_words: DF.SmallText | None
|
in_words: DF.SmallText | None
|
||||||
incoterm: DF.Link | None
|
incoterm: DF.Link | None
|
||||||
inter_company_invoice_reference: DF.Link | None
|
inter_company_invoice_reference: DF.Link | None
|
||||||
@@ -162,6 +163,7 @@ class SalesInvoice(SellingController):
|
|||||||
only_include_allocated_payments: DF.Check
|
only_include_allocated_payments: DF.Check
|
||||||
other_charges_calculation: DF.TextEditor | None
|
other_charges_calculation: DF.TextEditor | None
|
||||||
outstanding_amount: DF.Currency
|
outstanding_amount: DF.Currency
|
||||||
|
override_tax_withholding_entries: DF.Check
|
||||||
packed_items: DF.Table[PackedItem]
|
packed_items: DF.Table[PackedItem]
|
||||||
paid_amount: DF.Currency
|
paid_amount: DF.Currency
|
||||||
party_account_currency: DF.Link | None
|
party_account_currency: DF.Link | None
|
||||||
@@ -214,6 +216,8 @@ class SalesInvoice(SellingController):
|
|||||||
subscription: DF.Link | None
|
subscription: DF.Link | None
|
||||||
tax_category: DF.Link | None
|
tax_category: DF.Link | None
|
||||||
tax_id: DF.Data | None
|
tax_id: DF.Data | None
|
||||||
|
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||||
|
tax_withholding_group: DF.Link | None
|
||||||
taxes: DF.Table[SalesTaxesandCharges]
|
taxes: DF.Table[SalesTaxesandCharges]
|
||||||
taxes_and_charges: DF.Link | None
|
taxes_and_charges: DF.Link | None
|
||||||
tc_name: DF.Link | None
|
tc_name: DF.Link | None
|
||||||
@@ -282,58 +286,13 @@ class SalesInvoice(SellingController):
|
|||||||
self.indicator_color = "green"
|
self.indicator_color = "green"
|
||||||
self.indicator_title = _("Paid")
|
self.indicator_title = _("Paid")
|
||||||
|
|
||||||
def before_print(self, settings=None):
|
def onload(self):
|
||||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
super().onload()
|
||||||
|
if self.customer:
|
||||||
super().before_print(settings)
|
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||||
|
"Customer", self.customer, ["tax_withholding_category", "tax_withholding_group"]
|
||||||
company_details = frappe.get_value(
|
|
||||||
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
|
||||||
)
|
|
||||||
|
|
||||||
required_fields = [
|
|
||||||
company_details.get("company_logo"),
|
|
||||||
company_details.get("phone_no"),
|
|
||||||
company_details.get("email"),
|
|
||||||
]
|
|
||||||
|
|
||||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
address_display_list = get_address_display_list("Company", self.company)
|
|
||||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
|
||||||
|
|
||||||
required_fields.append(self.company_address)
|
|
||||||
required_fields.append(address_line)
|
|
||||||
|
|
||||||
if not all(required_fields):
|
|
||||||
frappe.publish_realtime(
|
|
||||||
"sales_invoice_before_print",
|
|
||||||
{
|
|
||||||
"company_logo": company_details.get("company_logo"),
|
|
||||||
"website": company_details.get("website"),
|
|
||||||
"phone_no": company_details.get("phone_no"),
|
|
||||||
"email": company_details.get("email"),
|
|
||||||
"address_line": address_line,
|
|
||||||
"company": self.company,
|
|
||||||
"company_address": self.company_address,
|
|
||||||
"name": self.name,
|
|
||||||
},
|
|
||||||
user=frappe.session.user,
|
|
||||||
)
|
)
|
||||||
|
self.set_onload("apply_tds", tax_withholding_category or tax_withholding_group)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_auto_set_posting_time()
|
self.validate_auto_set_posting_time()
|
||||||
@@ -344,7 +303,7 @@ class SalesInvoice(SellingController):
|
|||||||
if not (self.is_pos or self.is_debit_note):
|
if not (self.is_pos or self.is_debit_note):
|
||||||
self.so_dn_required()
|
self.so_dn_required()
|
||||||
|
|
||||||
self.set_tax_withholding()
|
SalesTaxWithholding(self).on_validate()
|
||||||
|
|
||||||
self.validate_proj_cust()
|
self.validate_proj_cust()
|
||||||
self.validate_pos_return()
|
self.validate_pos_return()
|
||||||
@@ -466,38 +425,6 @@ class SalesInvoice(SellingController):
|
|||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
|
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
|
||||||
if self.get("is_opening") == "Yes":
|
|
||||||
return
|
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
|
||||||
|
|
||||||
if not tax_withholding_details:
|
|
||||||
return
|
|
||||||
|
|
||||||
accounts = []
|
|
||||||
tax_withholding_account = tax_withholding_details.get("account_head")
|
|
||||||
|
|
||||||
for d in self.taxes:
|
|
||||||
if d.account_head == tax_withholding_account:
|
|
||||||
d.update(tax_withholding_details)
|
|
||||||
accounts.append(d.account_head)
|
|
||||||
|
|
||||||
if not accounts or tax_withholding_account not in accounts:
|
|
||||||
self.append("taxes", tax_withholding_details)
|
|
||||||
|
|
||||||
to_remove = [
|
|
||||||
d
|
|
||||||
for d in self.taxes
|
|
||||||
if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in to_remove:
|
|
||||||
self.remove(d)
|
|
||||||
|
|
||||||
# calculate totals again after applying TDS
|
|
||||||
self.calculate_taxes_and_totals()
|
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.set_paid_amount()
|
self.set_paid_amount()
|
||||||
@@ -519,6 +446,8 @@ class SalesInvoice(SellingController):
|
|||||||
# NOTE status updating bypassed for is_return
|
# NOTE status updating bypassed for is_return
|
||||||
self.status_updater = []
|
self.status_updater = []
|
||||||
|
|
||||||
|
SalesTaxWithholding(self).on_submit()
|
||||||
|
|
||||||
self.update_status_updater_args()
|
self.update_status_updater_args()
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
|
|
||||||
@@ -658,6 +587,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||||
|
SalesTaxWithholding(self).on_cancel()
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
@@ -699,6 +629,7 @@ class SalesInvoice(SellingController):
|
|||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
|
"Tax Withholding Entry",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.delete_auto_created_batches()
|
self.delete_auto_created_batches()
|
||||||
@@ -1678,7 +1609,11 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
|
if (
|
||||||
|
flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||||
|
or item.is_fixed_asset
|
||||||
|
or enable_discount_accounting
|
||||||
|
):
|
||||||
# Do not book income for transfer within same company
|
# Do not book income for transfer within same company
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
continue
|
continue
|
||||||
@@ -2375,6 +2310,7 @@ def get_list_context(context=None):
|
|||||||
"show_search": True,
|
"show_search": True,
|
||||||
"no_breadcrumbs": True,
|
"no_breadcrumbs": True,
|
||||||
"title": _("Invoices"),
|
"title": _("Invoices"),
|
||||||
|
"list_template": "templates/includes/list/list.html",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return list_context
|
return list_context
|
||||||
@@ -2948,59 +2884,6 @@ def get_loyalty_programs(customer):
|
|||||||
return lp_details
|
return lp_details
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_company_master_details(name, company, details):
|
|
||||||
from frappe.utils import validate_email_address
|
|
||||||
|
|
||||||
if isinstance(details, str):
|
|
||||||
details = frappe.parse_json(details)
|
|
||||||
|
|
||||||
if details.get("email"):
|
|
||||||
validate_email_address(details.get("email"), throw=True)
|
|
||||||
|
|
||||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
|
||||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
|
||||||
|
|
||||||
if company_fields_to_update:
|
|
||||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
|
||||||
|
|
||||||
company_address = details.get("company_address")
|
|
||||||
if details.get("address_line1"):
|
|
||||||
address_doc = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Address",
|
|
||||||
"address_title": details.get("address_title"),
|
|
||||||
"address_type": details.get("address_type"),
|
|
||||||
"address_line1": details.get("address_line1"),
|
|
||||||
"address_line2": details.get("address_line2"),
|
|
||||||
"city": details.get("city"),
|
|
||||||
"state": details.get("state"),
|
|
||||||
"pincode": details.get("pincode"),
|
|
||||||
"country": details.get("country"),
|
|
||||||
"is_your_company_address": 1,
|
|
||||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
address_doc.insert()
|
|
||||||
company_address = address_doc.name
|
|
||||||
|
|
||||||
if company_address:
|
|
||||||
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
|
|
||||||
if not company_address_display or details.get("address_line1"):
|
|
||||||
from frappe.query_builder import DocType
|
|
||||||
|
|
||||||
SalesInvoice = DocType("Sales Invoice")
|
|
||||||
|
|
||||||
(
|
|
||||||
frappe.qb.update(SalesInvoice)
|
|
||||||
.set(SalesInvoice.company_address, company_address)
|
|
||||||
.set(SalesInvoice.company_address_display, get_address_display(company_address))
|
|
||||||
.where(SalesInvoice.name == name)
|
|
||||||
).run()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_invoice_discounting(source_name, target_doc=None):
|
def create_invoice_discounting(source_name, target_doc=None):
|
||||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
set_default_account_for_mode_of_payment(
|
set_default_account_for_mode_of_payment(
|
||||||
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
|
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
|
||||||
)
|
)
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
for company in frappe.get_all("Company", pluck="name"):
|
||||||
|
frappe.db.set_value("Company", company, "accounts_frozen_till_date", None)
|
||||||
|
|
||||||
@change_settings(
|
@change_settings(
|
||||||
"Accounts Settings",
|
"Accounts Settings",
|
||||||
@@ -3398,8 +3399,8 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
si.commission_rate = commission_rate
|
si.commission_rate = commission_rate
|
||||||
self.assertRaises(frappe.ValidationError, si.save)
|
self.assertRaises(frappe.ValidationError, si.save)
|
||||||
|
|
||||||
@IntegrationTestCase.change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
|
||||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||||
|
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
|
||||||
si = create_sales_invoice(do_not_save=True)
|
si = create_sales_invoice(do_not_save=True)
|
||||||
si.posting_date = add_days(getdate(), 1)
|
si.posting_date = add_days(getdate(), 1)
|
||||||
si.save()
|
si.save()
|
||||||
@@ -3407,6 +3408,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
self.assertRaises(frappe.ValidationError, si.submit)
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
si.posting_date = getdate()
|
si.posting_date = getdate()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||||
|
|
||||||
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||||
def test_over_billing_case_against_delivery_note(self):
|
def test_over_billing_case_against_delivery_note(self):
|
||||||
@@ -3473,7 +3475,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", getdate("2019-01-31"))
|
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31"))
|
||||||
|
|
||||||
pda1 = frappe.get_doc(
|
pda1 = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
|
|||||||
@@ -43,12 +43,14 @@
|
|||||||
"rate",
|
"rate",
|
||||||
"amount",
|
"amount",
|
||||||
"item_tax_template",
|
"item_tax_template",
|
||||||
|
"tax_withholding_category",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"base_rate",
|
"base_rate",
|
||||||
"base_amount",
|
"base_amount",
|
||||||
"pricing_rules",
|
"pricing_rules",
|
||||||
"stock_uom_rate",
|
"stock_uom_rate",
|
||||||
"is_free_item",
|
"is_free_item",
|
||||||
|
"apply_tds",
|
||||||
"grant_commission",
|
"grant_commission",
|
||||||
"section_break_21",
|
"section_break_21",
|
||||||
"net_rate",
|
"net_rate",
|
||||||
@@ -986,6 +988,21 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "SCIO Detail",
|
"label": "SCIO Detail",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_category",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Tax Withholding Category",
|
||||||
|
"options": "Tax Withholding Category",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "apply_tds",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consider for Tax Withholding",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class SalesInvoiceItem(Document):
|
|||||||
actual_qty: DF.Float
|
actual_qty: DF.Float
|
||||||
allow_zero_valuation_rate: DF.Check
|
allow_zero_valuation_rate: DF.Check
|
||||||
amount: DF.Currency
|
amount: DF.Currency
|
||||||
|
apply_tds: DF.Check
|
||||||
asset: DF.Link | None
|
asset: DF.Link | None
|
||||||
barcode: DF.Data | None
|
barcode: DF.Data | None
|
||||||
base_amount: DF.Currency
|
base_amount: DF.Currency
|
||||||
@@ -95,6 +96,7 @@ class SalesInvoiceItem(Document):
|
|||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
stock_uom_rate: DF.Currency
|
stock_uom_rate: DF.Currency
|
||||||
target_warehouse: DF.Link | None
|
target_warehouse: DF.Link | None
|
||||||
|
tax_withholding_category: DF.Link | None
|
||||||
total_weight: DF.Float
|
total_weight: DF.Float
|
||||||
uom: DF.Link
|
uom: DF.Link
|
||||||
use_serial_batch_fields: DF.Check
|
use_serial_batch_fields: DF.Check
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"included_in_print_rate",
|
"included_in_print_rate",
|
||||||
"included_in_paid_amount",
|
"included_in_paid_amount",
|
||||||
"set_by_item_tax_template",
|
"set_by_item_tax_template",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -202,7 +203,7 @@
|
|||||||
"fieldname": "dont_recompute_tax",
|
"fieldname": "dont_recompute_tax",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Dont Recompute tax",
|
"label": "Don't Recompute Tax",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -241,6 +242,13 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class SalesTaxesandCharges(Document):
|
|||||||
dont_recompute_tax: DF.Check
|
dont_recompute_tax: DF.Check
|
||||||
included_in_paid_amount: DF.Check
|
included_in_paid_amount: DF.Check
|
||||||
included_in_print_rate: DF.Check
|
included_in_print_rate: DF.Check
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
|
item_wise_tax_detail: DF.Code | None
|
||||||
net_amount: DF.Currency
|
net_amount: DF.Currency
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate",
|
"label": "Rate",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
@@ -91,15 +91,16 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:39.866399",
|
"modified": "2025-12-10 08:06:40.611761",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Share Balance",
|
"name": "Share Balance",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ShareBalance(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
amount: DF.Int
|
amount: DF.Currency
|
||||||
current_state: DF.Literal["", "Issued", "Purchased"]
|
current_state: DF.Literal["", "Issued", "Purchased"]
|
||||||
from_no: DF.Int
|
from_no: DF.Int
|
||||||
is_company: DF.Check
|
is_company: DF.Check
|
||||||
@@ -22,7 +22,7 @@ class ShareBalance(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
rate: DF.Int
|
rate: DF.Currency
|
||||||
share_type: DF.Link
|
share_type: DF.Link
|
||||||
to_no: DF.Int
|
to_no: DF.Int
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -411,7 +411,10 @@ class Subscription(Document):
|
|||||||
invoice.customer = self.party
|
invoice.customer = self.party
|
||||||
else:
|
else:
|
||||||
invoice.supplier = self.party
|
invoice.supplier = self.party
|
||||||
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
|
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||||
|
"Supplier", self.party, ["tax_withholding_category", "tax_withholding_group"]
|
||||||
|
)
|
||||||
|
if tax_withholding_category or tax_withholding_group:
|
||||||
invoice.apply_tds = 1
|
invoice.apply_tds = 1
|
||||||
|
|
||||||
# Add currency to invoice
|
# Add currency to invoice
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TestSubscription(IntegrationTestCase):
|
|||||||
make_plans()
|
make_plans()
|
||||||
create_parties()
|
create_parties()
|
||||||
reset_settings()
|
reset_settings()
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"autoname": "hash",
|
|
||||||
"creation": "2022-09-13 16:18:59.404842",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"voucher_type",
|
|
||||||
"voucher_name",
|
|
||||||
"taxable_amount"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "voucher_type",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Voucher Type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "voucher_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Voucher Name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "taxable_amount",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Taxable Amount",
|
|
||||||
"options": "Company:company:default_currency"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2025-02-05 16:39:14.863698",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Accounts",
|
|
||||||
"name": "Tax Withheld Vouchers",
|
|
||||||
"naming_rule": "Random",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "creation",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"states": []
|
|
||||||
}
|
|
||||||
@@ -16,4 +16,54 @@ frappe.ui.form.on("Tax Withholding Category", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refresh: function (frm) {
|
||||||
|
update_rates_read_only_state(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
disable_cumulative_threshold: function (frm) {
|
||||||
|
toggle_threshold_settings(frm, "disable_cumulative_threshold");
|
||||||
|
if (frm.doc.disable_cumulative_threshold) {
|
||||||
|
reset_rates_column(frm, "cumulative_threshold");
|
||||||
|
}
|
||||||
|
update_rates_read_only_state(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
disable_transaction_threshold: function (frm) {
|
||||||
|
toggle_threshold_settings(frm, "disable_transaction_threshold");
|
||||||
|
if (frm.doc.disable_transaction_threshold) {
|
||||||
|
reset_rates_column(frm, "single_threshold");
|
||||||
|
}
|
||||||
|
update_rates_read_only_state(frm);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggle_threshold_settings(frm, field_name) {
|
||||||
|
if (frm.doc[field_name]) {
|
||||||
|
const other_field =
|
||||||
|
field_name === "disable_cumulative_threshold"
|
||||||
|
? "disable_transaction_threshold"
|
||||||
|
: "disable_cumulative_threshold";
|
||||||
|
frm.set_value(other_field, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_rates_read_only_state(frm) {
|
||||||
|
frm.fields_dict["rates"].grid.update_docfield_property(
|
||||||
|
"cumulative_threshold",
|
||||||
|
"read_only",
|
||||||
|
frm.doc.disable_cumulative_threshold
|
||||||
|
);
|
||||||
|
frm.fields_dict["rates"].grid.update_docfield_property(
|
||||||
|
"single_threshold",
|
||||||
|
"read_only",
|
||||||
|
frm.doc.disable_transaction_threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_rates_column(frm, field_name) {
|
||||||
|
$.each(frm.doc.rates || [], function (i, row) {
|
||||||
|
row[field_name] = 0;
|
||||||
|
});
|
||||||
|
frm.refresh_field("rates");
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"category_details_section",
|
"category_details_section",
|
||||||
"category_name",
|
"category_name",
|
||||||
"round_off_tax_amount",
|
"tax_deduction_basis",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"consider_party_ledger_amount",
|
"round_off_tax_amount",
|
||||||
"tax_on_excess_amount",
|
"tax_on_excess_amount",
|
||||||
|
"disable_cumulative_threshold",
|
||||||
|
"disable_transaction_threshold",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"rates",
|
"rates",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
@@ -61,14 +63,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
"description": "Tax withheld only for amount exceeding cumulative threshold",
|
||||||
"fieldname": "consider_party_ledger_amount",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Consider Entire Party Ledger Amount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
|
|
||||||
"fieldname": "tax_on_excess_amount",
|
"fieldname": "tax_on_excess_amount",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Only Deduct Tax On Excess Amount "
|
"label": "Only Deduct Tax On Excess Amount "
|
||||||
@@ -79,6 +74,28 @@
|
|||||||
"fieldname": "round_off_tax_amount",
|
"fieldname": "round_off_tax_amount",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Round Off Tax Amount"
|
"label": "Round Off Tax Amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Net Total",
|
||||||
|
"fieldname": "tax_deduction_basis",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Deduct Tax On Basis",
|
||||||
|
"options": "\nGross Total\nNet Total",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "When checked, only transaction threshold will be applied for transaction individually",
|
||||||
|
"fieldname": "disable_cumulative_threshold",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disable Cumulative Threshold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "When checked, only cumulative threshold will be applied",
|
||||||
|
"fieldname": "disable_transaction_threshold",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disable Transaction Threshold"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.utils import getdate
|
||||||
from frappe.utils import cint, flt, getdate
|
|
||||||
|
|
||||||
|
from erpnext import allow_regional
|
||||||
from erpnext.controllers.accounts_controller import validate_account_head
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
|
|
||||||
|
|
||||||
@@ -28,30 +29,41 @@ class TaxWithholdingCategory(Document):
|
|||||||
|
|
||||||
accounts: DF.Table[TaxWithholdingAccount]
|
accounts: DF.Table[TaxWithholdingAccount]
|
||||||
category_name: DF.Data | None
|
category_name: DF.Data | None
|
||||||
consider_party_ledger_amount: DF.Check
|
disable_cumulative_threshold: DF.Check
|
||||||
|
disable_transaction_threshold: DF.Check
|
||||||
rates: DF.Table[TaxWithholdingRate]
|
rates: DF.Table[TaxWithholdingRate]
|
||||||
round_off_tax_amount: DF.Check
|
round_off_tax_amount: DF.Check
|
||||||
|
tax_deduction_basis: DF.Literal["", "Gross Total", "Net Total"]
|
||||||
tax_on_excess_amount: DF.Check
|
tax_on_excess_amount: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
# TODO: Disable single threshold if tax on excess is enabled
|
||||||
self.validate_dates()
|
self.validate_dates()
|
||||||
self.validate_companies_and_accounts()
|
self.validate_companies_and_accounts()
|
||||||
self.validate_thresholds()
|
self.validate_thresholds()
|
||||||
|
|
||||||
def validate_dates(self):
|
def validate_dates(self):
|
||||||
last_to_date = None
|
group_rates = defaultdict(list)
|
||||||
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
|
for d in self.get("rates"):
|
||||||
|
|
||||||
for d in rates:
|
|
||||||
if getdate(d.from_date) >= getdate(d.to_date):
|
if getdate(d.from_date) >= getdate(d.to_date):
|
||||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||||
|
group_rates[d.tax_withholding_group].append(d)
|
||||||
|
|
||||||
# validate overlapping of dates
|
# Validate overlapping dates within each group
|
||||||
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
|
for group, rates in group_rates.items():
|
||||||
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
|
rates = sorted(rates, key=lambda d: getdate(d.from_date))
|
||||||
|
last_to_date = None
|
||||||
|
|
||||||
last_to_date = d.to_date
|
for d in rates:
|
||||||
|
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0}: Dates overlapping with other row in group {1}").format(
|
||||||
|
d.idx, group or "Default"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
last_to_date = d.to_date
|
||||||
|
|
||||||
def validate_companies_and_accounts(self):
|
def validate_companies_and_accounts(self):
|
||||||
existing_accounts = set()
|
existing_accounts = set()
|
||||||
@@ -78,699 +90,168 @@ class TaxWithholdingCategory(Document):
|
|||||||
).format(d.idx)
|
).format(d.idx)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_applicable_tax_row(self, posting_date, tax_withholding_group):
|
||||||
|
for row in self.rates:
|
||||||
|
if (
|
||||||
|
getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date)
|
||||||
|
and row.tax_withholding_group == tax_withholding_group
|
||||||
|
):
|
||||||
|
return row
|
||||||
|
|
||||||
def get_party_details(inv):
|
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
||||||
party_type, party = "", ""
|
|
||||||
|
|
||||||
if inv.doctype == "Sales Invoice":
|
def get_company_account(self, company):
|
||||||
party_type = "Customer"
|
for row in self.accounts:
|
||||||
party = inv.customer
|
if company == row.company:
|
||||||
else:
|
return row.account
|
||||||
party_type = "Supplier"
|
|
||||||
party = inv.supplier
|
|
||||||
|
|
||||||
if not party:
|
|
||||||
frappe.throw(_("Please select {0} first").format(party_type))
|
|
||||||
|
|
||||||
return party_type, party
|
|
||||||
|
|
||||||
|
|
||||||
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|
||||||
if inv.doctype == "Payment Entry":
|
|
||||||
inv.tax_withholding_net_total = inv.net_total
|
|
||||||
inv.base_tax_withholding_net_total = inv.net_total
|
|
||||||
|
|
||||||
pan_no = ""
|
|
||||||
parties = []
|
|
||||||
party_type, party = get_party_details(inv)
|
|
||||||
has_pan_field = frappe.get_meta(party_type).has_field("pan")
|
|
||||||
|
|
||||||
if not tax_withholding_category:
|
|
||||||
if has_pan_field:
|
|
||||||
fields = ["tax_withholding_category", "pan"]
|
|
||||||
else:
|
|
||||||
fields = ["tax_withholding_category"]
|
|
||||||
|
|
||||||
tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1)
|
|
||||||
|
|
||||||
tax_withholding_category = tax_withholding_details.get("tax_withholding_category")
|
|
||||||
pan_no = tax_withholding_details.get("pan")
|
|
||||||
|
|
||||||
if not tax_withholding_category:
|
|
||||||
return
|
|
||||||
|
|
||||||
# if tax_withholding_category passed as an argument but not pan_no
|
|
||||||
if not pan_no and has_pan_field:
|
|
||||||
pan_no = frappe.db.get_value(party_type, party, "pan")
|
|
||||||
|
|
||||||
# Get others suppliers with the same PAN No
|
|
||||||
if pan_no:
|
|
||||||
parties = frappe.get_all(party_type, filters={"pan": pan_no}, pluck="name")
|
|
||||||
|
|
||||||
if not parties:
|
|
||||||
parties.append(party)
|
|
||||||
|
|
||||||
posting_date = inv.get("posting_date") or inv.get("transaction_date")
|
|
||||||
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
|
|
||||||
|
|
||||||
if not tax_details:
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"Skipping Tax Withholding Category {0} as there is no associated account set for Company {1} in it."
|
|
||||||
).format(tax_withholding_category, inv.company)
|
|
||||||
)
|
|
||||||
if inv.doctype == "Purchase Invoice":
|
|
||||||
return {}, [], {}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if party_type == "Customer" and not tax_details.cumulative_threshold:
|
|
||||||
# TCS is only chargeable on sum of invoiced value
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_("No Tax withholding account set for Company {0} in Tax Withholding Category {1}.").format(
|
||||||
"Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value."
|
frappe.bold(company), frappe.bold(self.name)
|
||||||
).format(tax_withholding_category, inv.company, party)
|
|
||||||
)
|
|
||||||
|
|
||||||
tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
|
|
||||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
|
||||||
)
|
|
||||||
|
|
||||||
if party_type == "Supplier":
|
|
||||||
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
|
||||||
else:
|
|
||||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
|
||||||
|
|
||||||
cost_center = get_cost_center(inv)
|
|
||||||
tax_row.update(
|
|
||||||
{
|
|
||||||
"cost_center": cost_center,
|
|
||||||
"is_tax_withholding_account": 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if cint(tax_details.round_off_tax_amount):
|
|
||||||
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
|
|
||||||
|
|
||||||
if inv.doctype == "Purchase Invoice":
|
|
||||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
|
||||||
else:
|
|
||||||
return tax_row
|
|
||||||
|
|
||||||
|
|
||||||
def get_cost_center(inv):
|
|
||||||
cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
|
|
||||||
|
|
||||||
if len(inv.get("taxes", [])) > 0:
|
|
||||||
cost_center = inv.get("taxes")[0].cost_center
|
|
||||||
|
|
||||||
return cost_center
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
|
|
||||||
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
|
|
||||||
|
|
||||||
tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date)
|
|
||||||
|
|
||||||
for account_detail in tax_withholding.accounts:
|
|
||||||
if company == account_detail.company:
|
|
||||||
return frappe._dict(
|
|
||||||
{
|
|
||||||
"tax_withholding_category": tax_withholding_category,
|
|
||||||
"account_head": account_detail.account,
|
|
||||||
"rate": tax_rate_detail.tax_withholding_rate,
|
|
||||||
"from_date": tax_rate_detail.from_date,
|
|
||||||
"to_date": tax_rate_detail.to_date,
|
|
||||||
"threshold": tax_rate_detail.single_threshold,
|
|
||||||
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
|
|
||||||
"description": tax_withholding.category_name
|
|
||||||
if tax_withholding.category_name
|
|
||||||
else tax_withholding_category,
|
|
||||||
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
|
|
||||||
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
|
|
||||||
"round_off_tax_amount": tax_withholding.round_off_tax_amount,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
|
||||||
# returns the row that matches with the fiscal year from posting date
|
|
||||||
for rate in tax_withholding.rates:
|
|
||||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
|
||||||
return rate
|
|
||||||
|
|
||||||
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
|
|
||||||
row = {
|
|
||||||
"category": "Total",
|
|
||||||
"charge_type": "Actual",
|
|
||||||
"tax_amount": tax_amount,
|
|
||||||
"description": tax_details.description,
|
|
||||||
"account_head": tax_details.account_head,
|
|
||||||
}
|
|
||||||
|
|
||||||
if tax_deducted:
|
|
||||||
# TCS already deducted on previous invoices
|
|
||||||
# So, TCS will be calculated by 'Previous Row Total'
|
|
||||||
|
|
||||||
taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
|
|
||||||
if taxes_excluding_tcs:
|
|
||||||
# chargeable amount is the total amount after other charges are applied
|
|
||||||
row.update(
|
|
||||||
{
|
|
||||||
"charge_type": "On Previous Row Total",
|
|
||||||
"row_id": len(taxes_excluding_tcs),
|
|
||||||
"rate": tax_details.rate,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# if only TCS is to be charged, then net total is chargeable amount
|
|
||||||
row.update({"charge_type": "On Net Total", "rate": tax_details.rate})
|
|
||||||
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_row_for_tds(tax_details, tax_amount):
|
|
||||||
return {
|
|
||||||
"category": "Total",
|
|
||||||
"charge_type": "Actual",
|
|
||||||
"tax_amount": tax_amount,
|
|
||||||
"add_deduct_tax": "Deduct",
|
|
||||||
"description": tax_details.description,
|
|
||||||
"account_head": tax_details.account_head,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
|
||||||
ldc_name = frappe.db.get_value(
|
|
||||||
"Lower Deduction Certificate",
|
|
||||||
{
|
|
||||||
"pan_no": pan_no,
|
|
||||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
|
||||||
"valid_from": ("<=", posting_date),
|
|
||||||
"valid_upto": (">=", posting_date),
|
|
||||||
"company": company,
|
|
||||||
},
|
|
||||||
"name",
|
|
||||||
)
|
|
||||||
|
|
||||||
if ldc_name:
|
|
||||||
return frappe.get_doc("Lower Deduction Certificate", ldc_name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
|
||||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
|
||||||
parties,
|
|
||||||
tax_details,
|
|
||||||
inv.company,
|
|
||||||
party_type=party_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
|
||||||
parties, tax_details, inv.company, party_type=party_type
|
|
||||||
)
|
|
||||||
|
|
||||||
advance_vouchers = get_advance_vouchers(
|
|
||||||
parties,
|
|
||||||
company=inv.company,
|
|
||||||
from_date=tax_details.from_date,
|
|
||||||
to_date=tax_details.to_date,
|
|
||||||
party_type=party_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
|
||||||
tax_deducted_on_advances = 0
|
|
||||||
|
|
||||||
if inv.doctype == "Purchase Invoice":
|
|
||||||
tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details)
|
|
||||||
|
|
||||||
tax_deducted = 0
|
|
||||||
if taxable_vouchers:
|
|
||||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
|
||||||
|
|
||||||
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
|
||||||
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
|
||||||
# rest of the below logic to function properly
|
|
||||||
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
|
||||||
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
|
||||||
if tax_deducted_on_advances:
|
|
||||||
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
|
||||||
|
|
||||||
tax_amount = 0
|
|
||||||
|
|
||||||
if party_type == "Supplier":
|
|
||||||
# if tds account is changed.
|
|
||||||
if not tax_deducted:
|
|
||||||
tax_deducted = is_tax_deducted_on_the_basis_of_inv(vouchers)
|
|
||||||
|
|
||||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
|
||||||
if tax_deducted:
|
|
||||||
net_total = inv.tax_withholding_net_total
|
|
||||||
if ldc:
|
|
||||||
limit_consumed = get_limit_consumed(ldc, parties)
|
|
||||||
if is_valid_certificate(ldc, posting_date, limit_consumed):
|
|
||||||
tax_amount = get_lower_deduction_amount(
|
|
||||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tax_amount = net_total * tax_details.rate / 100
|
|
||||||
else:
|
|
||||||
tax_amount = net_total * tax_details.rate / 100
|
|
||||||
|
|
||||||
# once tds is deducted, not need to add vouchers in the invoice
|
|
||||||
voucher_wise_amount = {}
|
|
||||||
else:
|
|
||||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount)
|
|
||||||
|
|
||||||
elif party_type == "Customer":
|
|
||||||
if tax_deducted:
|
|
||||||
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
|
||||||
tax_amount = 0
|
|
||||||
else:
|
|
||||||
# if no TCS has been charged in FY,
|
|
||||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
|
||||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
|
||||||
|
|
||||||
if cint(tax_details.round_off_tax_amount):
|
|
||||||
tax_amount = normal_round(tax_amount)
|
|
||||||
|
|
||||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
|
||||||
|
|
||||||
|
|
||||||
def is_tax_deducted_on_the_basis_of_inv(vouchers):
|
|
||||||
return frappe.db.exists(
|
|
||||||
"Purchase Taxes and Charges",
|
|
||||||
{
|
|
||||||
"parent": ["in", vouchers],
|
|
||||||
"is_tax_withholding_account": 1,
|
|
||||||
"parenttype": "Purchase Invoice",
|
|
||||||
"base_tax_amount_after_discount_amount": [">", 0],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
|
||||||
voucher_wise_amount = []
|
|
||||||
vouchers = []
|
|
||||||
|
|
||||||
ldcs = frappe.db.get_all(
|
|
||||||
"Lower Deduction Certificate",
|
|
||||||
filters={
|
|
||||||
"valid_from": [">=", tax_details.from_date],
|
|
||||||
"valid_upto": ["<=", tax_details.to_date],
|
|
||||||
"company": company,
|
|
||||||
"supplier": ["in", parties],
|
|
||||||
},
|
|
||||||
fields=["supplier", "valid_from", "valid_upto", "rate"],
|
|
||||||
)
|
|
||||||
|
|
||||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
|
||||||
field = [
|
|
||||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
|
||||||
"name",
|
|
||||||
"grand_total",
|
|
||||||
"posting_date",
|
|
||||||
]
|
|
||||||
|
|
||||||
filters = {
|
|
||||||
"company": company,
|
|
||||||
frappe.scrub(party_type): ["in", parties],
|
|
||||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
|
||||||
"is_opening": "No",
|
|
||||||
"docstatus": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if doctype != "Sales Invoice":
|
|
||||||
filters.update(
|
|
||||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
|
||||||
|
|
||||||
for d in invoices_details:
|
class TaxWithholdingDetails:
|
||||||
d = frappe._dict(
|
def __init__(
|
||||||
{
|
self,
|
||||||
"voucher_name": d.name,
|
tax_withholding_categories: list[str],
|
||||||
"voucher_type": doctype,
|
tax_withholding_group: str,
|
||||||
"taxable_amount": d.base_net_total,
|
posting_date: str,
|
||||||
"grand_total": d.grand_total,
|
party_type: str,
|
||||||
"posting_date": d.posting_date,
|
party: str,
|
||||||
}
|
company: str,
|
||||||
)
|
):
|
||||||
|
self.tax_withholding_categories = tax_withholding_categories
|
||||||
|
self.tax_withholding_group = tax_withholding_group
|
||||||
|
self.posting_date = posting_date
|
||||||
|
self.party_type = party_type
|
||||||
|
self.party = party
|
||||||
|
self.company = company
|
||||||
|
|
||||||
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
|
def get(self) -> list:
|
||||||
if ldc[0].supplier in parties and ldc[0].rate == 0:
|
|
||||||
d.update({"taxable_amount": 0})
|
|
||||||
|
|
||||||
vouchers.append(d.voucher_name)
|
|
||||||
voucher_wise_amount.append(d)
|
|
||||||
|
|
||||||
journal_entries_details = frappe.db.sql(
|
|
||||||
"""
|
"""
|
||||||
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
|
Fetches tax withholding categories based on the provided parameters.
|
||||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
"""
|
||||||
WHERE
|
category_details = frappe._dict()
|
||||||
j.name = ja.parent
|
if not self.tax_withholding_categories:
|
||||||
AND j.docstatus = 1
|
return category_details
|
||||||
AND j.is_opening = 'No'
|
|
||||||
AND j.posting_date between %s and %s
|
|
||||||
AND ja.party in %s
|
|
||||||
AND j.apply_tds = 1
|
|
||||||
AND j.tax_withholding_category = %s
|
|
||||||
AND j.company = %s
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
tax_details.from_date,
|
|
||||||
tax_details.to_date,
|
|
||||||
tuple(parties),
|
|
||||||
tax_details.get("tax_withholding_category"),
|
|
||||||
company,
|
|
||||||
),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in journal_entries_details:
|
ldc_details = self.get_ldc_details()
|
||||||
vouchers.append(d.name)
|
|
||||||
voucher_wise_amount.append(
|
for category_name in self.tax_withholding_categories:
|
||||||
frappe._dict(
|
doc: TaxWithholdingCategory = frappe.get_cached_doc("Tax Withholding Category", category_name)
|
||||||
{
|
row = doc.get_applicable_tax_row(self.posting_date, self.tax_withholding_group)
|
||||||
"voucher_name": d.name,
|
account_head = doc.get_company_account(self.company)
|
||||||
"voucher_type": "Journal Entry",
|
|
||||||
"taxable_amount": d.amount,
|
category_detail = frappe._dict(
|
||||||
"reference_type": d.reference_type,
|
name=category_name,
|
||||||
}
|
description=doc.category_name,
|
||||||
|
account_head=account_head,
|
||||||
|
# rates
|
||||||
|
tax_rate=row.tax_withholding_rate,
|
||||||
|
from_date=row.from_date,
|
||||||
|
to_date=row.to_date,
|
||||||
|
single_threshold=row.single_threshold,
|
||||||
|
cumulative_threshold=row.cumulative_threshold,
|
||||||
|
# settings
|
||||||
|
tax_deduction_basis=doc.tax_deduction_basis,
|
||||||
|
round_off_tax_amount=doc.round_off_tax_amount,
|
||||||
|
tax_on_excess_amount=doc.tax_on_excess_amount,
|
||||||
|
disable_cumulative_threshold=doc.disable_cumulative_threshold,
|
||||||
|
disable_transaction_threshold=doc.disable_transaction_threshold,
|
||||||
|
taxable_amount=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ldc (only if valid based on posting date)
|
||||||
|
if ldc_detail := ldc_details.get(category_name):
|
||||||
|
category_detail.update(ldc_detail)
|
||||||
|
|
||||||
|
category_details[category_name] = category_detail
|
||||||
|
|
||||||
|
return category_details
|
||||||
|
|
||||||
|
def get_ldc_details(self):
|
||||||
|
"""
|
||||||
|
Fetches the Lower Deduction Certificate (LDC) details for the given party.
|
||||||
|
Assumes that only one LDC per category can be valid at a time.
|
||||||
|
"""
|
||||||
|
ldc_details = {}
|
||||||
|
|
||||||
|
if self.party_type != "Supplier":
|
||||||
|
return ldc_details
|
||||||
|
|
||||||
|
# NOTE: This can be a configurable option
|
||||||
|
# To check if filter by tax_id is needed
|
||||||
|
tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||||
|
|
||||||
|
# ldc details
|
||||||
|
ldc_records = self.get_valid_ldc_records(tax_id)
|
||||||
|
if not ldc_records:
|
||||||
|
return ldc_details
|
||||||
|
|
||||||
|
ldc_names = [ldc.name for ldc in ldc_records]
|
||||||
|
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id)
|
||||||
|
|
||||||
|
# map
|
||||||
|
for ldc in ldc_records:
|
||||||
|
category_name = ldc.tax_withholding_category
|
||||||
|
|
||||||
|
unutilized_amount = ldc.certificate_limit - (ldc_utilization_map.get(ldc.name) or 0)
|
||||||
|
if not unutilized_amount:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ldc_details[category_name] = dict(
|
||||||
|
ldc_certificate=ldc.name,
|
||||||
|
ldc_unutilized_amount=unutilized_amount,
|
||||||
|
ldc_rate=ldc.rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ldc_details
|
||||||
|
|
||||||
|
def get_valid_ldc_records(self, tax_id):
|
||||||
|
ldc = frappe.qb.DocType("Lower Deduction Certificate")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(ldc)
|
||||||
|
.select(
|
||||||
|
ldc.name,
|
||||||
|
ldc.tax_withholding_category,
|
||||||
|
ldc.rate,
|
||||||
|
ldc.certificate_limit,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(ldc.valid_from <= self.posting_date)
|
||||||
|
& (ldc.valid_upto >= self.posting_date)
|
||||||
|
& (ldc.company == self.company)
|
||||||
|
& ldc.tax_withholding_category.isin(self.tax_withholding_categories)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return vouchers, voucher_wise_amount
|
query = query.where(ldc.pan_no == tax_id) if tax_id else query.where(ldc.supplier == self.party)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
def get_ldc_utilization_by_category(self, ldc_names, tax_id):
|
||||||
payment_entry_filters = {
|
twe = frappe.qb.DocType("Tax Withholding Entry")
|
||||||
"party_type": party_type,
|
query = (
|
||||||
"party": ("in", parties),
|
frappe.qb.from_(twe)
|
||||||
"docstatus": 1,
|
.select(twe.lower_deduction_certificate, Sum(twe.taxable_amount).as_("limit_consumed"))
|
||||||
"apply_tax_withholding_amount": 1,
|
.where(
|
||||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
(twe.company == self.company)
|
||||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
& (twe.party_type == self.party_type)
|
||||||
"company": company,
|
& (twe.tax_withholding_category.isin(self.tax_withholding_categories))
|
||||||
}
|
& (twe.lower_deduction_certificate.isin(ldc_names))
|
||||||
|
& (twe.docstatus == 1)
|
||||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
& (twe.status.isin(["Settled", "Over Withheld"]))
|
||||||
|
|
||||||
|
|
||||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
|
||||||
"""
|
|
||||||
Use Payment Ledger to fetch unallocated Advance Payments
|
|
||||||
"""
|
|
||||||
|
|
||||||
if party_type == "Supplier":
|
|
||||||
return []
|
|
||||||
|
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
|
||||||
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
conditions.append(ple.amount.lt(0))
|
|
||||||
conditions.append(ple.delinked == 0)
|
|
||||||
conditions.append(ple.party_type == party_type)
|
|
||||||
conditions.append(ple.party.isin(parties))
|
|
||||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
|
||||||
|
|
||||||
if company:
|
|
||||||
conditions.append(ple.company == company)
|
|
||||||
|
|
||||||
if from_date and to_date:
|
|
||||||
conditions.append(ple.posting_date[from_date:to_date])
|
|
||||||
|
|
||||||
advances = qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1)
|
|
||||||
if advances:
|
|
||||||
advances = [x[0] for x in advances]
|
|
||||||
|
|
||||||
return advances
|
|
||||||
|
|
||||||
|
|
||||||
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
|
||||||
tax_info = []
|
|
||||||
|
|
||||||
if inv.get("advances"):
|
|
||||||
advances = [d.reference_name for d in inv.get("advances")]
|
|
||||||
|
|
||||||
if advances:
|
|
||||||
pe = frappe.qb.DocType("Payment Entry").as_("pe")
|
|
||||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
|
||||||
|
|
||||||
tax_info = (
|
|
||||||
frappe.qb.from_(at)
|
|
||||||
.inner_join(pe)
|
|
||||||
.on(pe.name == at.parent)
|
|
||||||
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
|
||||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
|
||||||
.where(at.parent.isin(advances))
|
|
||||||
.where(at.account_head == tax_details.account_head)
|
|
||||||
.run(as_dict=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return tax_info
|
|
||||||
|
|
||||||
|
|
||||||
def get_deducted_tax(taxable_vouchers, tax_details):
|
|
||||||
# check if TDS / TCS account is already charged on taxable vouchers
|
|
||||||
filters = {
|
|
||||||
"is_cancelled": 0,
|
|
||||||
"credit": [">", 0],
|
|
||||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
|
||||||
"account": tax_details.account_head,
|
|
||||||
"voucher_no": ["in", taxable_vouchers],
|
|
||||||
}
|
|
||||||
field = "credit"
|
|
||||||
|
|
||||||
entries = frappe.db.get_all("GL Entry", filters, pluck=field)
|
|
||||||
return sum(entries)
|
|
||||||
|
|
||||||
|
|
||||||
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
|
||||||
"""
|
|
||||||
Only applies for Taxes deducted on Advance Payments
|
|
||||||
"""
|
|
||||||
advance_tax_from_across_fiscal_year = sum(
|
|
||||||
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
|
||||||
)
|
|
||||||
return advance_tax_from_across_fiscal_year
|
|
||||||
|
|
||||||
|
|
||||||
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
|
|
||||||
tds_amount = 0
|
|
||||||
|
|
||||||
pi_grand_total = 0
|
|
||||||
pi_base_net_total = 0
|
|
||||||
jv_credit_amt = 0
|
|
||||||
pe_credit_amt = 0
|
|
||||||
|
|
||||||
for row in voucher_wise_amount:
|
|
||||||
if row.voucher_type == "Purchase Invoice":
|
|
||||||
pi_grand_total += row.get("grand_total", 0)
|
|
||||||
pi_base_net_total += row.get("taxable_amount", 0)
|
|
||||||
|
|
||||||
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
|
|
||||||
jv_credit_amt += row.get("taxable_amount", 0)
|
|
||||||
|
|
||||||
## for TDS to be deducted on advances
|
|
||||||
pe_filters = {
|
|
||||||
"party_type": "Supplier",
|
|
||||||
"party": ("in", parties),
|
|
||||||
"docstatus": 1,
|
|
||||||
"apply_tax_withholding_amount": 1,
|
|
||||||
"unallocated_amount": (">", 0),
|
|
||||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
|
||||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
|
||||||
"company": inv.company,
|
|
||||||
}
|
|
||||||
|
|
||||||
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
|
|
||||||
|
|
||||||
if consider_party_ledger_amt:
|
|
||||||
pe_filters.pop("apply_tax_withholding_amount", None)
|
|
||||||
pe_filters.pop("tax_withholding_category", None)
|
|
||||||
|
|
||||||
# Get Amount via payment entry
|
|
||||||
payment_entries = frappe.db.get_all(
|
|
||||||
"Payment Entry",
|
|
||||||
filters=pe_filters,
|
|
||||||
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
|
|
||||||
)
|
|
||||||
|
|
||||||
for row in payment_entries:
|
|
||||||
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
|
|
||||||
pe_credit_amt += value
|
|
||||||
voucher_wise_amount.append(
|
|
||||||
frappe._dict(
|
|
||||||
{
|
|
||||||
"voucher_name": row.name,
|
|
||||||
"voucher_type": "Payment Entry",
|
|
||||||
"taxable_amount": value,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
.groupby(twe.lower_deduction_certificate)
|
||||||
)
|
)
|
||||||
|
|
||||||
threshold = tax_details.get("threshold", 0)
|
query = query.where(twe.tax_id == tax_id) if tax_id else query.where(twe.party == self.party)
|
||||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
|
||||||
supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0)
|
|
||||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
|
||||||
|
|
||||||
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
|
return frappe._dict(query.run())
|
||||||
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
|
|
||||||
|
|
||||||
cumulative_threshold_breached = (
|
|
||||||
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
|
|
||||||
)
|
|
||||||
|
|
||||||
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
|
|
||||||
supp_credit_amt += pi_base_net_total
|
|
||||||
|
|
||||||
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
|
||||||
supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold
|
|
||||||
|
|
||||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
|
||||||
tds_amount = get_lower_deduction_amount(
|
|
||||||
supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
|
||||||
|
|
||||||
return tds_amount
|
|
||||||
|
|
||||||
|
|
||||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
@allow_regional
|
||||||
tcs_amount = 0
|
def get_tax_id_for_party(party_type, party):
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
return None
|
||||||
|
|
||||||
# sum of debit entries made from sales invoices
|
|
||||||
invoiced_amt = (
|
|
||||||
frappe.db.get_value(
|
|
||||||
"GL Entry",
|
|
||||||
{
|
|
||||||
"is_cancelled": 0,
|
|
||||||
"party_type": "Customer",
|
|
||||||
"party": ["in", parties],
|
|
||||||
"company": inv.company,
|
|
||||||
"voucher_no": ["in", vouchers],
|
|
||||||
},
|
|
||||||
[{"SUM": "debit"}],
|
|
||||||
)
|
|
||||||
or 0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# sum of credit entries made from PE / JV with unset 'against voucher'
|
|
||||||
|
|
||||||
conditions = []
|
|
||||||
conditions.append(ple.amount.lt(0))
|
|
||||||
conditions.append(ple.delinked == 0)
|
|
||||||
conditions.append(ple.party_type == "Customer")
|
|
||||||
conditions.append(ple.party.isin(parties))
|
|
||||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
|
||||||
conditions.append(ple.company == inv.company)
|
|
||||||
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
|
|
||||||
|
|
||||||
advance_amt = (
|
|
||||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# sum of credit entries made from sales invoice
|
|
||||||
credit_note_amt = sum(
|
|
||||||
frappe.db.get_all(
|
|
||||||
"GL Entry",
|
|
||||||
{
|
|
||||||
"is_cancelled": 0,
|
|
||||||
"credit": [">", 0],
|
|
||||||
"party_type": "Customer",
|
|
||||||
"party": ["in", parties],
|
|
||||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
|
||||||
"company": inv.company,
|
|
||||||
"voucher_type": "Sales Invoice",
|
|
||||||
},
|
|
||||||
pluck="credit",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
|
||||||
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
|
||||||
|
|
||||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
|
||||||
total_invoiced_amt = (
|
|
||||||
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
|
||||||
)
|
|
||||||
|
|
||||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
|
||||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
|
||||||
tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
|
|
||||||
|
|
||||||
return tcs_amount
|
|
||||||
|
|
||||||
|
|
||||||
def get_advance_adjusted_in_invoice(inv):
|
|
||||||
advances_adjusted = 0
|
|
||||||
for row in inv.get("advances", []):
|
|
||||||
advances_adjusted += row.allocated_amount
|
|
||||||
|
|
||||||
return advances_adjusted
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_total_without_tcs(inv, tax_details):
|
|
||||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
|
||||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
|
||||||
|
|
||||||
return inv.grand_total - tcs_tax_row_amount
|
|
||||||
|
|
||||||
|
|
||||||
def get_limit_consumed(ldc, parties):
|
|
||||||
limit_consumed = frappe.db.get_value(
|
|
||||||
"Purchase Invoice",
|
|
||||||
{
|
|
||||||
"supplier": ("in", parties),
|
|
||||||
"apply_tds": 1,
|
|
||||||
"docstatus": 1,
|
|
||||||
"tax_withholding_category": ldc.tax_withholding_category,
|
|
||||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
|
||||||
"company": ldc.company,
|
|
||||||
},
|
|
||||||
[{"SUM": "tax_withholding_net_total"}],
|
|
||||||
)
|
|
||||||
|
|
||||||
return limit_consumed
|
|
||||||
|
|
||||||
|
|
||||||
def get_lower_deduction_amount(current_amount, limit_consumed, certificate_limit, rate, tax_details):
|
|
||||||
if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
|
|
||||||
return current_amount * rate / 100
|
|
||||||
else:
|
|
||||||
ltds_amount = certificate_limit - flt(limit_consumed)
|
|
||||||
tds_amount = current_amount - ltds_amount
|
|
||||||
|
|
||||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_certificate(ldc, posting_date, limit_consumed):
|
|
||||||
available_amount = flt(ldc.certificate_limit) - flt(limit_consumed)
|
|
||||||
if (getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)) and available_amount > 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def normal_round(number):
|
|
||||||
"""
|
|
||||||
Rounds a number to the nearest integer.
|
|
||||||
:param number: The number to round.
|
|
||||||
"""
|
|
||||||
decimal_part = number - int(number)
|
|
||||||
|
|
||||||
if decimal_part >= 0.5:
|
|
||||||
decimal_part = 1
|
|
||||||
else:
|
|
||||||
decimal_part = 0
|
|
||||||
|
|
||||||
number = int(number) + decimal_part
|
|
||||||
|
|
||||||
return number
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2025-06-20 04:55:28.583171",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"section_break_krko",
|
||||||
|
"company",
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"tax_id",
|
||||||
|
"column_break_egzm",
|
||||||
|
"tax_withholding_category",
|
||||||
|
"tax_withholding_group",
|
||||||
|
"taxable_amount",
|
||||||
|
"tax_rate",
|
||||||
|
"withholding_amount",
|
||||||
|
"target_section",
|
||||||
|
"taxable_doctype",
|
||||||
|
"taxable_name",
|
||||||
|
"taxable_date",
|
||||||
|
"currency",
|
||||||
|
"conversion_rate",
|
||||||
|
"column_break_fqoe",
|
||||||
|
"under_withheld_reason",
|
||||||
|
"lower_deduction_certificate",
|
||||||
|
"source_section",
|
||||||
|
"withholding_doctype",
|
||||||
|
"withholding_name",
|
||||||
|
"withholding_date",
|
||||||
|
"column_break_dahw",
|
||||||
|
"section_break_ggna",
|
||||||
|
"status",
|
||||||
|
"column_break_jfjf",
|
||||||
|
"created_by_migration"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_krko",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Party Type",
|
||||||
|
"options": "DocType",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Party",
|
||||||
|
"options": "party_type",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Tax ID",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_category",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Withholding Category",
|
||||||
|
"options": "Tax Withholding Category",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_egzm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "tax_rate",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "taxable_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Base Taxable Amount",
|
||||||
|
"options": "Company:company:default_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Transaction from which tax is withheld",
|
||||||
|
"fieldname": "source_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Deducted From"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dahw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Transaction for which tax is withheld",
|
||||||
|
"fieldname": "target_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Applicable For"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fqoe",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ggna",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_jfjf",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lower_deduction_certificate",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Lower Deduction Certificate",
|
||||||
|
"options": "Lower Deduction Certificate",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"options": "\nSettled\nUnder Withheld\nOver Withheld\nDuplicate\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "conversion_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Exchange Rate",
|
||||||
|
"precision": "9",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "withholding_doctype",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Withholding Document Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "withholding_name",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Withholding Document Name",
|
||||||
|
"options": "withholding_doctype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxable_doctype",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Taxable Document Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxable_name",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Taxable Document Name",
|
||||||
|
"options": "taxable_doctype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxable_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Taxable Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "withholding_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Withholding Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "under_withheld_reason",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Under Withheld Reason",
|
||||||
|
"options": "\nThreshold Exemption\nLower Deduction Certificate",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "withholding_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Base Tax Withheld",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "tax_withholding_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Withholding Group",
|
||||||
|
"options": "Tax Withholding Group",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "created_by_migration",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Created By Migration",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-12-22 09:07:26.701207",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Tax Withholding Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestTaxWithholdingEntry(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for TaxWithholdingEntry.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Tax Withholding Group", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:group_name",
|
||||||
|
"creation": "2025-06-29 05:24:51.819891",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"group_name"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "group_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Group Name",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-06-29 05:25:50.243710",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Tax Withholding Group",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class TaxWithheldVouchers(Document):
|
class TaxWithholdingGroup(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
@@ -14,12 +14,7 @@ class TaxWithheldVouchers(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
parent: DF.Data
|
group_name: DF.Data
|
||||||
parentfield: DF.Data
|
|
||||||
parenttype: DF.Data
|
|
||||||
taxable_amount: DF.Currency
|
|
||||||
voucher_name: DF.Data | None
|
|
||||||
voucher_type: DF.Data | None
|
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestTaxWithholdingGroup(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for TaxWithholdingGroup.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -7,10 +7,11 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"from_date",
|
"from_date",
|
||||||
"to_date",
|
"to_date",
|
||||||
"tax_withholding_rate",
|
"tax_withholding_group",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"single_threshold",
|
"tax_withholding_rate",
|
||||||
"cumulative_threshold"
|
"cumulative_threshold",
|
||||||
|
"single_threshold"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -30,14 +31,14 @@
|
|||||||
"fieldname": "single_threshold",
|
"fieldname": "single_threshold",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Single Transaction Threshold"
|
"label": "Transaction Threshold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 3,
|
"columns": 3,
|
||||||
"fieldname": "cumulative_threshold",
|
"fieldname": "cumulative_threshold",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Cumulative Transaction Threshold"
|
"label": "Cumulative Threshold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
@@ -54,20 +55,28 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "To Date",
|
"label": "To Date",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Withholding Group",
|
||||||
|
"options": "Tax Withholding Group"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:52.708165",
|
"modified": "2025-06-29 05:31:05.120377",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withholding Rate",
|
"name": "Tax Withholding Rate",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class TaxWithholdingRate(Document):
|
|||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
single_threshold: DF.Float
|
single_threshold: DF.Float
|
||||||
|
tax_withholding_group: DF.Link | None
|
||||||
tax_withholding_rate: DF.Float
|
tax_withholding_rate: DF.Float
|
||||||
to_date: DF.Date
|
to_date: DF.Date
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -210,19 +210,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
|
|||||||
for d in gl_map:
|
for d in gl_map:
|
||||||
cost_center = d.get("cost_center")
|
cost_center = d.get("cost_center")
|
||||||
|
|
||||||
|
cost_center_allocation = get_cost_center_allocation_data(
|
||||||
|
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||||
|
)
|
||||||
|
|
||||||
|
if not cost_center_allocation:
|
||||||
|
new_gl_map.append(d)
|
||||||
|
continue
|
||||||
|
|
||||||
# Validate budget against main cost center
|
# Validate budget against main cost center
|
||||||
if not from_repost:
|
if not from_repost:
|
||||||
validate_expense_against_budget(
|
validate_expense_against_budget(
|
||||||
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
||||||
)
|
)
|
||||||
|
|
||||||
cost_center_allocation = get_cost_center_allocation_data(
|
|
||||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
|
||||||
)
|
|
||||||
if not cost_center_allocation:
|
|
||||||
new_gl_map.append(d)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if d.account == round_off_account:
|
if d.account == round_off_account:
|
||||||
d.cost_center = cost_center_allocation[0][0]
|
d.cost_center = cost_center_allocation[0][0]
|
||||||
new_gl_map.append(d)
|
new_gl_map.append(d)
|
||||||
@@ -406,7 +407,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
|||||||
|
|
||||||
dimension_filter_map = get_dimension_filter_map()
|
dimension_filter_map = get_dimension_filter_map()
|
||||||
if gl_map:
|
if gl_map:
|
||||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
check_freezing_date(gl_map[0]["posting_date"], gl_map[0]["company"], adv_adj)
|
||||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
||||||
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
|
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
|
||||||
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||||
@@ -426,7 +427,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
|||||||
gle.flags.notify_update = False
|
gle.flags.notify_update = False
|
||||||
gle.submit()
|
gle.submit()
|
||||||
|
|
||||||
if not from_repost and gle.voucher_type != "Period Closing Voucher":
|
if (
|
||||||
|
not from_repost
|
||||||
|
and gle.voucher_type != "Period Closing Voucher"
|
||||||
|
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
|
||||||
|
):
|
||||||
validate_expense_against_budget(args)
|
validate_expense_against_budget(args)
|
||||||
|
|
||||||
|
|
||||||
@@ -767,7 +772,7 @@ def make_reverse_gl_entries(
|
|||||||
make_entry(new_gle, adv_adj, "Yes")
|
make_entry(new_gle, adv_adj, "Yes")
|
||||||
|
|
||||||
|
|
||||||
def check_freezing_date(posting_date, adv_adj=False):
|
def check_freezing_date(posting_date, company, adv_adj=False):
|
||||||
"""
|
"""
|
||||||
Nobody can do GL Entries where posting date is before freezing date
|
Nobody can do GL Entries where posting date is before freezing date
|
||||||
except authorized person
|
except authorized person
|
||||||
@@ -776,17 +781,17 @@ def check_freezing_date(posting_date, adv_adj=False):
|
|||||||
Hence stop admin to bypass if accounts are freezed
|
Hence stop admin to bypass if accounts are freezed
|
||||||
"""
|
"""
|
||||||
if not adv_adj:
|
if not adv_adj:
|
||||||
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
|
||||||
if acc_frozen_upto:
|
if acc_frozen_till_date:
|
||||||
frozen_accounts_modifier = frappe.get_single_value(
|
frozen_accounts_modifier = frappe.db.get_value(
|
||||||
"Accounts Settings", "frozen_accounts_modifier"
|
"Company", company, "role_allowed_for_frozen_entries"
|
||||||
)
|
)
|
||||||
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
|
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
|
||||||
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("You are not authorized to add or update entries before {0}").format(
|
_("You are not authorized to add or update entries before {0}").format(
|
||||||
formatdate(acc_frozen_upto)
|
formatdate(acc_frozen_till_date)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +1,108 @@
|
|||||||
<style>
|
<style>
|
||||||
.letter-head {
|
.letter-head {
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.letter-head td{
|
.letter-head td {
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
}
|
}
|
||||||
.invoice-header {
|
.invoice-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.logo-cell {
|
.logo-cell {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.logo-container {
|
.logo-container {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.logo-container img {
|
.logo-container img {
|
||||||
max-width: 90px;
|
max-width: 90px;
|
||||||
max-height: 90px;
|
max-height: 90px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
.company-details {
|
.company-details {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
}
|
||||||
.company-name {
|
.company-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #171717;
|
color: #171717;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.invoice-info-cell {
|
.invoice-info-cell {
|
||||||
float: right;
|
float: right;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.invoice-info {
|
.invoice-info {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.invoice-label {
|
.invoice-label {
|
||||||
color: #7C7C7C;
|
color: #7c7c7c;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 60px;
|
margin-right: 5px;
|
||||||
margin-right: 5px;
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<table class="invoice-header">
|
<table class="invoice-header">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="logo-cell" style="vertical-align: middle !important;">
|
<td class="logo-cell" style="vertical-align: middle !important">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
|
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
|
||||||
{% if company_logo %}
|
company_logo %}
|
||||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
|
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="company-details">
|
<td class="company-details">
|
||||||
<div class="company-name">
|
<div class="company-name">{{ doc.company }}</div>
|
||||||
{{ doc.company }}
|
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
|
||||||
</div>
|
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
|
||||||
{% if doc.company_address %}
|
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
|
||||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2", "city",
|
||||||
|
"state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address %} {{
|
||||||
|
company_address.address_line1 or "" }}<br />
|
||||||
|
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
|
||||||
|
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
|
||||||
|
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
{{ company_address.get("address_line1") or "" }}<br>
|
<td class="invoice-info-cell">
|
||||||
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
|
{% set website = frappe.db.get_value("Company", doc.company, "website") %} {% set email =
|
||||||
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
|
frappe.db.get_value("Company", doc.company, "email") %} {% set phone_no =
|
||||||
{% endif %}
|
frappe.db.get_value("Company", doc.company, "phone_no") %}
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="invoice-info-cell">
|
<div class="invoice-info">
|
||||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
<span class="invoice-label">{{ doc.doctype }}</span>
|
||||||
|
<span>{{ doc.name }}</span>
|
||||||
<div class="invoice-info">
|
</div>
|
||||||
<span class="invoice-label">{{ _("Invoice:") }}</span>
|
{% if website %}
|
||||||
<span>{{ doc.name }}</span>
|
<div class="invoice-info">
|
||||||
</div>
|
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||||
{% if company_details.website %}
|
<span>{{ website }}</span>
|
||||||
<div class="invoice-info">
|
</div>
|
||||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
{% endif %} {% if email %}
|
||||||
<span>{{ company_details.website }}</span>
|
<div class="invoice-info">
|
||||||
</div>
|
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||||
{% endif %}
|
<span>{{ email }}</span>
|
||||||
{% if company_details.email %}
|
</div>
|
||||||
<div class="invoice-info">
|
{% endif %} {% if phone_no %}
|
||||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
<div class="invoice-info">
|
||||||
<span>{{ company_details.email }}</span>
|
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||||
</div>
|
<span>{{ phone_no }}</span>
|
||||||
{% endif %}
|
</div>
|
||||||
{% if company_details.phone_no %}
|
{% endif %}
|
||||||
<div class="invoice-info">
|
</td>
|
||||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
</tr>
|
||||||
<span>{{ company_details.phone_no }}</span>
|
</tbody>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<style>
|
<style>
|
||||||
.print-format-preview {
|
.print-format-preview {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.letter-head {
|
.letter-head {
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
@@ -82,41 +82,43 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="company-name">{{ doc.company }}</div>
|
<div class="company-name">{{ doc.company }}</div>
|
||||||
<div class="company-address">
|
<div class="company-address">
|
||||||
{% if doc.company_address %}
|
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
|
||||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
|
||||||
{{ company_address.address_line1 or "" }}<br />
|
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
|
||||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
|
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2",
|
||||||
{{ company_address.city or "" }}, {{ company_address.state or "" }}
|
"city", "state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address
|
||||||
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
%} {{ company_address.address_line1 or "" }}<br />
|
||||||
|
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
|
||||||
|
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
|
||||||
|
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="vertical-align: top">
|
<td style="vertical-align: top">
|
||||||
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
||||||
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
|
<div class="invoice-title">{{ doc.doctype }}</div>
|
||||||
<div class="invoice-number">{{ doc.name }}</div>
|
<div class="invoice-number">{{ doc.name }}</div>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: left; float: right" class="other-details">
|
<div style="text-align: left; float: right" class="other-details">
|
||||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email",
|
||||||
{% if company_details.website %}
|
"phone_no"], as_dict=True) %} {% set website = company_details.website %} {% set email =
|
||||||
<div>
|
company_details.email %} {% set phone_no = company_details.phone_no %} {% if website %}
|
||||||
<span class="contact-title">{{ _("Website:") }}</span
|
<div>
|
||||||
><span class="contact-value">{{ company_details.website }}</span>
|
<span class="contact-title">{{ _("Website:") }}</span
|
||||||
</div>
|
><span class="contact-value">{{ website }}</span>
|
||||||
{% endif %}
|
</div>
|
||||||
{% if company_details.email %}
|
{% endif %} {% if email %}
|
||||||
<div>
|
<div>
|
||||||
<span class="contact-title">{{ _("Email:") }}</span
|
<span class="contact-title">{{ _("Email:") }}</span
|
||||||
><span class="contact-value">{{ company_details.email }}</span>
|
><span class="contact-value">{{ email }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {% if phone_no %}
|
||||||
{% if company_details.phone_no %}
|
<div>
|
||||||
<div>
|
<span class="contact-title">{{ _("Contact:") }}</span
|
||||||
<span class="contact-title">{{ _("Contact:") }}</span
|
><span class="contact-value">{{ phone_no }}</span>
|
||||||
><span class="contact-value">{{ company_details.phone_no }}</span>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -176,10 +176,6 @@ def _get_party_details(
|
|||||||
for d in party.get("sales_team")
|
for d in party.get("sales_team")
|
||||||
]
|
]
|
||||||
|
|
||||||
# supplier tax withholding category
|
|
||||||
if party_type == "Supplier" and party:
|
|
||||||
party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category")
|
|
||||||
|
|
||||||
if not party_details.get("tax_category") and pos_profile:
|
if not party_details.get("tax_category") and pos_profile:
|
||||||
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
|
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
|
||||||
|
|
||||||
@@ -352,10 +348,13 @@ def set_contact_details(party_details, party, party_type):
|
|||||||
|
|
||||||
def set_other_values(party_details, party, party_type):
|
def set_other_values(party_details, party, party_type):
|
||||||
# copy
|
# copy
|
||||||
|
to_copy = ["tax_withholding_category", "tax_withholding_group", "language"]
|
||||||
|
|
||||||
if party_type == "Customer":
|
if party_type == "Customer":
|
||||||
to_copy = ["customer_name", "customer_group", "territory", "language"]
|
to_copy.extend(["customer_name", "customer_group", "territory"])
|
||||||
else:
|
else:
|
||||||
to_copy = ["supplier_name", "supplier_group", "language"]
|
to_copy.extend(["supplier_name", "supplier_group"])
|
||||||
|
|
||||||
for f in to_copy:
|
for f in to_copy:
|
||||||
party_details[f] = party.get(f)
|
party_details[f] = party.get(f)
|
||||||
|
|
||||||
@@ -795,7 +794,7 @@ def get_payment_terms_template(party_name, party_type, company=None):
|
|||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def validate_party_frozen_disabled(party_type, party_name):
|
def validate_party_frozen_disabled(company, party_type, party_name):
|
||||||
if frappe.flags.ignore_party_validation:
|
if frappe.flags.ignore_party_validation:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -805,10 +804,10 @@ def validate_party_frozen_disabled(party_type, party_name):
|
|||||||
if party.disabled:
|
if party.disabled:
|
||||||
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
|
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
|
||||||
elif party.get("is_frozen"):
|
elif party.get("is_frozen"):
|
||||||
frozen_accounts_modifier = frappe.get_single_value(
|
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||||
"Accounts Settings", "frozen_accounts_modifier"
|
"Company", company, "role_allowed_for_frozen_entries"
|
||||||
)
|
)
|
||||||
if frozen_accounts_modifier not in frappe.get_roles():
|
if role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||||
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
|
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
|
||||||
|
|
||||||
elif party_type == "Employee":
|
elif party_type == "Employee":
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1384,27 +1384,14 @@ class InitSQLProceduresForAR:
|
|||||||
amount_in_account_currency {_currency_type}) engine=memory;
|
amount_in_account_currency {_currency_type}) engine=memory;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Function
|
|
||||||
genkey_function_name = "ar_genkey"
|
|
||||||
genkey_function_sql = f"""
|
|
||||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
|
||||||
begin
|
|
||||||
if allocate then
|
|
||||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
|
||||||
else
|
|
||||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
|
||||||
end if;
|
|
||||||
end
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Procedures
|
# Procedures
|
||||||
init_procedure_name = "ar_init_tmp_table"
|
init_procedure_name = "ar_init_tmp_table"
|
||||||
init_procedure_sql = f"""
|
init_procedure_sql = f"""
|
||||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||||
begin
|
begin
|
||||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
|
||||||
then
|
then
|
||||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||||
end if;
|
end if;
|
||||||
end;
|
end;
|
||||||
"""
|
"""
|
||||||
@@ -1446,16 +1433,13 @@ class InitSQLProceduresForAR:
|
|||||||
|
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||||
end;
|
end;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
existing_procedures = frappe.db.get_routines()
|
existing_procedures = frappe.db.get_routines()
|
||||||
|
|
||||||
if self.genkey_function_name not in existing_procedures:
|
|
||||||
frappe.db.sql(self.genkey_function_sql)
|
|
||||||
|
|
||||||
if self.init_procedure_name not in existing_procedures:
|
if self.init_procedure_name not in existing_procedures:
|
||||||
frappe.db.sql(self.init_procedure_sql)
|
frappe.db.sql(self.init_procedure_sql)
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
|
|||||||
party_type = self.filters.party_type
|
party_type = self.filters.party_type
|
||||||
|
|
||||||
doctype = qb.DocType(party_type)
|
doctype = qb.DocType(party_type)
|
||||||
|
|
||||||
|
party_details_fields = [
|
||||||
|
doctype.name.as_("party"),
|
||||||
|
f"{scrub(party_type)}_name",
|
||||||
|
f"{scrub(party_type)}_group",
|
||||||
|
]
|
||||||
|
|
||||||
|
if party_type == "Customer":
|
||||||
|
party_details_fields.append(doctype.territory)
|
||||||
|
|
||||||
conditions = self.get_party_conditions(doctype)
|
conditions = self.get_party_conditions(doctype)
|
||||||
query = (
|
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
|
||||||
qb.from_(doctype)
|
|
||||||
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
|
|
||||||
.where(Criterion.all(conditions))
|
|
||||||
)
|
|
||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
|
|||||||
|
|
||||||
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
|
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
|
||||||
|
|
||||||
|
if self.filters.party_type == "Customer":
|
||||||
|
columns += [
|
||||||
|
{
|
||||||
|
"label": _("Customer Group"),
|
||||||
|
"fieldname": "customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Customer Group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Territory"),
|
||||||
|
"fieldname": "territory",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Territory",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
columns += [
|
||||||
|
{
|
||||||
|
"label": _("Supplier Group"),
|
||||||
|
"fieldname": "supplier_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Supplier Group",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
columns += [
|
columns += [
|
||||||
{
|
{
|
||||||
"label": _("Opening Balance"),
|
"label": _("Opening Balance"),
|
||||||
@@ -214,35 +245,6 @@ class PartyLedgerSummaryReport:
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Hidden columns for handling 'User Permissions'
|
|
||||||
if self.filters.party_type == "Customer":
|
|
||||||
columns += [
|
|
||||||
{
|
|
||||||
"label": _("Territory"),
|
|
||||||
"fieldname": "territory",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Territory",
|
|
||||||
"hidden": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Customer Group"),
|
|
||||||
"fieldname": "customer_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Customer Group",
|
|
||||||
"hidden": 1,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
columns += [
|
|
||||||
{
|
|
||||||
"label": _("Supplier Group"),
|
|
||||||
"fieldname": "supplier_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Supplier Group",
|
|
||||||
"hidden": 1,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100})
|
columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100})
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
|
|||||||
expected = {
|
expected = {
|
||||||
"party": "_Test Customer",
|
"party": "_Test Customer",
|
||||||
"customer_name": "_Test Customer",
|
"customer_name": "_Test Customer",
|
||||||
|
"customer_group": "_Test Customer Group",
|
||||||
|
"territory": "_Test Territory",
|
||||||
"party_name": "_Test Customer",
|
"party_name": "_Test Customer",
|
||||||
"opening_balance": 0,
|
"opening_balance": 0,
|
||||||
"invoiced_amount": 100.0,
|
"invoiced_amount": 100.0,
|
||||||
@@ -213,6 +215,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
|
|||||||
expected = {
|
expected = {
|
||||||
"party": "_Test Customer",
|
"party": "_Test Customer",
|
||||||
"customer_name": "_Test Customer",
|
"customer_name": "_Test Customer",
|
||||||
|
"customer_group": "_Test Customer Group",
|
||||||
|
"territory": "_Test Territory",
|
||||||
"party_name": "_Test Customer",
|
"party_name": "_Test Customer",
|
||||||
"opening_balance": 0,
|
"opening_balance": 0,
|
||||||
"invoiced_amount": 100.0,
|
"invoiced_amount": 100.0,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from pypika import Order
|
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||||
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
tax_doctype="Purchase Taxes and Charges",
|
tax_doctype="Purchase Taxes and Charges",
|
||||||
)
|
)
|
||||||
|
|
||||||
scrubbed_tax_fields = {}
|
|
||||||
|
|
||||||
for tax in tax_columns:
|
|
||||||
scrubbed_tax_fields.update(
|
|
||||||
{
|
|
||||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
|
||||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
for tax, details in itemised_tax.get(d.name, {}).items():
|
for tax, details in itemised_tax.get(d.name, {}).items():
|
||||||
row.update(
|
row.update(
|
||||||
{
|
{
|
||||||
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
|
f"{tax}_rate": details.get("tax_rate", 0),
|
||||||
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
|
f"{tax}_amount": details.get("tax_amount", 0),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if details.get("is_other_charges"):
|
if details.get("is_other_charges"):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import functions as fn
|
from frappe.query_builder import functions as fn
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import flt
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
from frappe.utils.xlsxutils import handle_html
|
from frappe.utils.xlsxutils import handle_html
|
||||||
|
|
||||||
@@ -32,15 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
|||||||
return columns, [], None, None, None, 0
|
return columns, [], None, None, None, 0
|
||||||
|
|
||||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||||
scrubbed_tax_fields = {}
|
|
||||||
|
|
||||||
for tax in tax_columns:
|
|
||||||
scrubbed_tax_fields.update(
|
|
||||||
{
|
|
||||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
|
||||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||||
@@ -101,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
|||||||
for tax, details in itemised_tax.get(d.name, {}).items():
|
for tax, details in itemised_tax.get(d.name, {}).items():
|
||||||
row.update(
|
row.update(
|
||||||
{
|
{
|
||||||
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
|
f"{tax}_rate": details.get("tax_rate", 0),
|
||||||
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
|
f"{tax}_amount": details.get("tax_amount", 0),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if details.get("is_other_charges"):
|
if details.get("is_other_charges"):
|
||||||
@@ -567,15 +558,24 @@ def get_tax_accounts(
|
|||||||
tax_details = query.run(as_dict=True)
|
tax_details = query.run(as_dict=True)
|
||||||
|
|
||||||
precision = frappe.get_precision(tax_doctype, "tax_amount", currency=company_currency) or 2
|
precision = frappe.get_precision(tax_doctype, "tax_amount", currency=company_currency) or 2
|
||||||
tax_columns = set()
|
tax_columns = {}
|
||||||
itemised_tax = {}
|
itemised_tax = {}
|
||||||
|
scrubbed_description_map = {}
|
||||||
|
|
||||||
for row in tax_details:
|
for row in tax_details:
|
||||||
description = handle_html(row.description) or row.account_head
|
description = handle_html(row.description) or row.account_head
|
||||||
|
scrubbed_description = scrubbed_description_map.get(description)
|
||||||
|
if not scrubbed_description:
|
||||||
|
scrubbed_description = frappe.scrub(description)
|
||||||
|
scrubbed_description_map[description] = scrubbed_description
|
||||||
|
|
||||||
|
if scrubbed_description not in tax_columns and row.amount:
|
||||||
|
# as description is text editor earlier and markup can break the column convention in reports
|
||||||
|
tax_columns[scrubbed_description] = description
|
||||||
|
|
||||||
rate = "NA" if row.rate == 0 else row.rate
|
rate = "NA" if row.rate == 0 else row.rate
|
||||||
tax_columns.add(description)
|
|
||||||
itemised_tax.setdefault(row.item_row, {}).setdefault(
|
itemised_tax.setdefault(row.item_row, {}).setdefault(
|
||||||
description,
|
scrubbed_description,
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"tax_rate": rate,
|
"tax_rate": rate,
|
||||||
@@ -585,14 +585,16 @@ def get_tax_accounts(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
itemised_tax[row.item_row][description].tax_amount += flt(row.amount, precision)
|
itemised_tax[row.item_row][scrubbed_description].tax_amount += flt(row.amount, precision)
|
||||||
|
|
||||||
tax_columns = sorted(tax_columns)
|
tax_columns_list = list(tax_columns.keys())
|
||||||
for desc in tax_columns:
|
tax_columns_list.sort()
|
||||||
|
for scrubbed_desc in tax_columns_list:
|
||||||
|
desc = tax_columns[scrubbed_desc]
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(desc + " Rate"),
|
"label": _(desc + " Rate"),
|
||||||
"fieldname": frappe.scrub(desc + " Rate"),
|
"fieldname": f"{scrubbed_desc}_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
}
|
}
|
||||||
@@ -601,7 +603,7 @@ def get_tax_accounts(
|
|||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(desc + " Amount"),
|
"label": _(desc + " Amount"),
|
||||||
"fieldname": frappe.scrub(desc + " Amount"),
|
"fieldname": f"{scrubbed_desc}_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
@@ -639,7 +641,7 @@ def get_tax_accounts(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return itemised_tax, tax_columns
|
return itemised_tax, tax_columns_list
|
||||||
|
|
||||||
|
|
||||||
def get_tax_details_query(doctype, tax_doctype):
|
def get_tax_details_query(doctype, tax_doctype):
|
||||||
@@ -756,5 +758,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
|
|||||||
total_row["percent_gt"] += item["percent_gt"]
|
total_row["percent_gt"] += item["percent_gt"]
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
|
total_row.setdefault(f"{tax}_amount", 0.0)
|
||||||
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
|
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ frappe.query_reports["Tax Withholding Details"] = {
|
|||||||
fieldname: "party_type",
|
fieldname: "party_type",
|
||||||
label: __("Party Type"),
|
label: __("Party Type"),
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
options: ["Supplier", "Customer"],
|
options: ["", "Supplier", "Customer"],
|
||||||
reqd: 1,
|
default: "",
|
||||||
default: "Supplier",
|
|
||||||
on_change: function () {
|
on_change: function () {
|
||||||
frappe.query_report.set_filter_value("party", "");
|
frappe.query_report.set_filter_value("party", "");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,194 +1,112 @@
|
|||||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt, getdate
|
from frappe.query_builder.functions import IfNull
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_currency_precision
|
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
if filters.get("party_type") == "Customer":
|
"""Generate Tax Withholding Details report"""
|
||||||
party_naming_by = frappe.get_single_value("Selling Settings", "cust_master_name")
|
|
||||||
else:
|
|
||||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
|
||||||
|
|
||||||
filters["naming_series"] = party_naming_by
|
|
||||||
|
|
||||||
validate_filters(filters)
|
validate_filters(filters)
|
||||||
(
|
|
||||||
tds_docs,
|
|
||||||
tds_accounts,
|
|
||||||
tax_category_map,
|
|
||||||
journal_entry_party_map,
|
|
||||||
net_total_map,
|
|
||||||
) = get_tds_docs(filters)
|
|
||||||
|
|
||||||
|
# Process and format data
|
||||||
|
data = get_tax_withholding_data(filters)
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
|
|
||||||
res = get_result(
|
return columns, data
|
||||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
|
|
||||||
)
|
|
||||||
return columns, res
|
|
||||||
|
|
||||||
|
|
||||||
def validate_filters(filters):
|
def validate_filters(filters):
|
||||||
"""Validate if dates are properly set"""
|
"""Validate report filters"""
|
||||||
filters = frappe._dict(filters or {})
|
filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
if not filters.from_date or not filters.to_date:
|
||||||
|
frappe.throw(_("From Date and To Date are required"))
|
||||||
|
|
||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
frappe.throw(_("From Date must be before To Date"))
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
|
|
||||||
|
|
||||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map):
|
def get_tax_withholding_data(filters):
|
||||||
party_map = get_party_pan_map(filters.get("party_type"))
|
"""Process entries into final report format"""
|
||||||
tax_rate_map = get_tax_rate_map(filters)
|
data = []
|
||||||
gle_map = get_gle_map(tds_docs)
|
entries = get_tax_withholding_entries(filters)
|
||||||
precision = get_currency_precision()
|
if not entries:
|
||||||
|
return data
|
||||||
|
|
||||||
out = []
|
doc_info = get_additional_doc_info(entries)
|
||||||
entries = {}
|
party_details = get_party_details(entries)
|
||||||
for name, details in gle_map.items():
|
|
||||||
for entry in details:
|
|
||||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
|
||||||
tax_withholding_category, rate = None, None
|
|
||||||
bill_no, bill_date = "", ""
|
|
||||||
party = entry.party or entry.against
|
|
||||||
posting_date = entry.posting_date
|
|
||||||
voucher_type = entry.voucher_type
|
|
||||||
|
|
||||||
if voucher_type == "Journal Entry":
|
for entry in entries:
|
||||||
party_list = journal_entry_party_map.get(name)
|
doc_details = frappe._dict()
|
||||||
if party_list:
|
if entry.taxable_name:
|
||||||
party = party_list[0]
|
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
|
||||||
|
|
||||||
if entry.account in tds_accounts.keys():
|
party_info = party_details.get((entry.party_type, entry.party), {})
|
||||||
tax_amount += entry.credit - entry.debit
|
|
||||||
# infer tax withholding category from the account if it's the single account for this category
|
|
||||||
tax_withholding_category = tds_accounts.get(entry.account)
|
|
||||||
# or else the consolidated value from the voucher document
|
|
||||||
if not tax_withholding_category:
|
|
||||||
tax_withholding_category = tax_category_map.get((voucher_type, name))
|
|
||||||
# or else from the party default
|
|
||||||
if not tax_withholding_category:
|
|
||||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
|
||||||
|
|
||||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
row = {
|
||||||
|
"section_code": entry.tax_withholding_category,
|
||||||
|
"entity_type": party_info.get("entity_type"),
|
||||||
|
"rate": entry.tax_rate,
|
||||||
|
"total_amount": entry.taxable_amount,
|
||||||
|
"grand_total": doc_details.get("grand_total", 0),
|
||||||
|
"base_total": doc_details.get("base_total", 0),
|
||||||
|
"tax_amount": entry.withholding_amount,
|
||||||
|
"transaction_date": entry.withholding_date,
|
||||||
|
"transaction_type": entry.taxable_doctype,
|
||||||
|
"ref_no": entry.taxable_name,
|
||||||
|
"taxable_date": entry.taxable_date,
|
||||||
|
"supplier_invoice_no": doc_details.get("bill_no"),
|
||||||
|
"supplier_invoice_date": doc_details.get("bill_date"),
|
||||||
|
"withholding_doctype": entry.withholding_doctype,
|
||||||
|
"withholding_name": entry.withholding_name,
|
||||||
|
"party_name": party_info.get("party_name"),
|
||||||
|
"tax_id": entry.tax_id,
|
||||||
|
"party": entry.party,
|
||||||
|
"party_type": entry.party_type,
|
||||||
|
}
|
||||||
|
data.append(row)
|
||||||
|
|
||||||
values = net_total_map.get((voucher_type, name))
|
# Sort by section code and transaction date
|
||||||
|
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
|
||||||
if values:
|
return data
|
||||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
|
||||||
# back calculate total amount from rate and tax_amount
|
|
||||||
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
|
|
||||||
total_amount = grand_total = base_total
|
|
||||||
|
|
||||||
else:
|
|
||||||
if tax_amount and rate:
|
|
||||||
# back calculate total amount from rate and tax_amount
|
|
||||||
total_amount = flt((tax_amount * 100) / rate, precision=precision)
|
|
||||||
else:
|
|
||||||
total_amount = values[0]
|
|
||||||
|
|
||||||
grand_total = values[1]
|
|
||||||
base_total = values[2]
|
|
||||||
|
|
||||||
if voucher_type == "Purchase Invoice":
|
|
||||||
bill_no = values[3]
|
|
||||||
bill_date = values[4]
|
|
||||||
else:
|
|
||||||
total_amount += entry.credit
|
|
||||||
|
|
||||||
if tax_amount:
|
|
||||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
|
||||||
party_name = "supplier_name"
|
|
||||||
party_type = "supplier_type"
|
|
||||||
else:
|
|
||||||
party_name = "customer_name"
|
|
||||||
party_type = "customer_type"
|
|
||||||
|
|
||||||
row = {
|
|
||||||
"pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id": party_map.get(
|
|
||||||
party, {}
|
|
||||||
).get("pan"),
|
|
||||||
"party": party_map.get(party, {}).get("name"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.naming_series == "Naming Series":
|
|
||||||
row["party_name"] = party_map.get(party, {}).get(party_name)
|
|
||||||
|
|
||||||
row.update(
|
|
||||||
{
|
|
||||||
"section_code": tax_withholding_category or "",
|
|
||||||
"entity_type": party_map.get(party, {}).get(party_type),
|
|
||||||
"rate": rate,
|
|
||||||
"total_amount": total_amount,
|
|
||||||
"grand_total": grand_total,
|
|
||||||
"base_total": base_total,
|
|
||||||
"tax_amount": tax_amount,
|
|
||||||
"transaction_date": posting_date,
|
|
||||||
"transaction_type": voucher_type,
|
|
||||||
"ref_no": name,
|
|
||||||
"supplier_invoice_no": bill_no,
|
|
||||||
"supplier_invoice_date": bill_date,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
key = entry.voucher_no
|
|
||||||
if key in entries:
|
|
||||||
entries[key]["tax_amount"] += tax_amount
|
|
||||||
else:
|
|
||||||
entries[key] = row
|
|
||||||
out = list(entries.values())
|
|
||||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def get_party_pan_map(party_type):
|
def get_party_details(entries):
|
||||||
|
"""Fetch party details in batch for all entries"""
|
||||||
party_map = frappe._dict()
|
party_map = frappe._dict()
|
||||||
|
parties_by_type = {"Customer": set(), "Supplier": set()}
|
||||||
|
|
||||||
fields = ["name", "tax_withholding_category"]
|
# Group parties by type
|
||||||
if party_type == "Supplier":
|
for entry in entries:
|
||||||
fields += ["supplier_type", "supplier_name"]
|
if entry.party_type in parties_by_type and entry.party:
|
||||||
else:
|
parties_by_type[entry.party_type].add(entry.party)
|
||||||
fields += ["customer_type", "customer_name"]
|
|
||||||
|
|
||||||
if frappe.db.has_column(party_type, "pan"):
|
# Batch fetch for each party type
|
||||||
fields.append("pan")
|
for party_type, party_set in parties_by_type.items():
|
||||||
|
if not party_type or not party_set:
|
||||||
|
continue
|
||||||
|
|
||||||
party_details = frappe.db.get_all(party_type, fields=fields)
|
doctype = frappe.qb.DocType(party_type)
|
||||||
|
fields = [doctype.name]
|
||||||
|
|
||||||
for party in party_details:
|
if party_type == "Supplier":
|
||||||
party.party_type = party_type
|
fields.extend([doctype.supplier_type.as_("entity_type"), doctype.supplier_name.as_("party_name")])
|
||||||
party_map[party.name] = party
|
elif party_type == "Customer":
|
||||||
|
fields.extend([doctype.customer_type.as_("entity_type"), doctype.customer_name.as_("party_name")])
|
||||||
|
|
||||||
|
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
|
||||||
|
party_details = query.run(as_dict=True)
|
||||||
|
|
||||||
|
for party in party_details:
|
||||||
|
party_map[(party_type, party.name)] = party
|
||||||
|
|
||||||
return party_map
|
return party_map
|
||||||
|
|
||||||
|
|
||||||
def get_gle_map(documents):
|
|
||||||
# create gle_map of the form
|
|
||||||
# {"purchase_invoice": list of dict of all gle created for this invoice}
|
|
||||||
gle_map = {}
|
|
||||||
|
|
||||||
gle = frappe.db.get_all(
|
|
||||||
"GL Entry",
|
|
||||||
{"voucher_no": ["in", documents], "is_cancelled": 0},
|
|
||||||
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in gle:
|
|
||||||
if d.voucher_no not in gle_map:
|
|
||||||
gle_map[d.voucher_no] = [d]
|
|
||||||
else:
|
|
||||||
gle_map[d.voucher_no].append(d)
|
|
||||||
|
|
||||||
return gle_map
|
|
||||||
|
|
||||||
|
|
||||||
def get_columns(filters):
|
def get_columns(filters):
|
||||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
"""Generate report columns based on filters"""
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
"label": _("Section Code"),
|
"label": _("Section Code"),
|
||||||
@@ -197,286 +115,190 @@ def get_columns(filters):
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
},
|
},
|
||||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
|
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
|
||||||
|
{
|
||||||
|
"label": _(f"{filters.get('party_type', 'Party')} Name"),
|
||||||
|
"fieldname": "party_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _(filters.get("party_type", "Party")),
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "party_type",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Entity Type"),
|
||||||
|
"fieldname": "entity_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Supplier Invoice No"),
|
||||||
|
"fieldname": "supplier_invoice_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Supplier Invoice Date"),
|
||||||
|
"fieldname": "supplier_invoice_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Tax Rate %"),
|
||||||
|
"fieldname": "rate",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"width": 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Total Amount"),
|
||||||
|
"fieldname": "total_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Base Total"),
|
||||||
|
"fieldname": "base_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Tax Amount"),
|
||||||
|
"fieldname": "tax_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Grand Total"),
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Reference Date"),
|
||||||
|
"fieldname": "taxable_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Transaction Type"),
|
||||||
|
"fieldname": "transaction_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Reference No."),
|
||||||
|
"fieldname": "ref_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "transaction_type",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Date of Transaction"),
|
||||||
|
"fieldname": "transaction_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Withholding Document"),
|
||||||
|
"fieldname": "withholding_name",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "withholding_doctype",
|
||||||
|
"width": 150,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if filters.naming_series == "Naming Series":
|
|
||||||
columns.append(
|
|
||||||
{
|
|
||||||
"label": _(filters.party_type + " Name"),
|
|
||||||
"fieldname": "party_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"width": 180,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
columns.append(
|
|
||||||
{
|
|
||||||
"label": _(filters.get("party_type")),
|
|
||||||
"fieldname": "party",
|
|
||||||
"fieldtype": "Dynamic Link",
|
|
||||||
"options": "party_type",
|
|
||||||
"width": 180,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
columns.extend(
|
|
||||||
[
|
|
||||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if filters.party_type == "Supplier":
|
|
||||||
columns.extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"label": _("Supplier Invoice No"),
|
|
||||||
"fieldname": "supplier_invoice_no",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Supplier Invoice Date"),
|
|
||||||
"fieldname": "supplier_invoice_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
columns.extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
|
||||||
"fieldname": "rate",
|
|
||||||
"fieldtype": "Percent",
|
|
||||||
"width": 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Total Amount"),
|
|
||||||
"fieldname": "total_amount",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Base Total"),
|
|
||||||
"fieldname": "base_total",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
|
|
||||||
"fieldname": "tax_amount",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Grand Total"),
|
|
||||||
"fieldname": "grand_total",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
|
|
||||||
{
|
|
||||||
"label": _("Reference No."),
|
|
||||||
"fieldname": "ref_no",
|
|
||||||
"fieldtype": "Dynamic Link",
|
|
||||||
"options": "transaction_type",
|
|
||||||
"width": 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": _("Date of Transaction"),
|
|
||||||
"fieldname": "transaction_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"width": 100,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_tds_docs(filters):
|
def get_tax_withholding_entries(filters):
|
||||||
tds_documents = []
|
twe = frappe.qb.DocType("Tax Withholding Entry")
|
||||||
purchase_invoices = []
|
|
||||||
sales_invoices = []
|
|
||||||
payment_entries = []
|
|
||||||
journal_entries = []
|
|
||||||
tax_category_map = frappe._dict()
|
|
||||||
net_total_map = frappe._dict()
|
|
||||||
journal_entry_party_map = frappe._dict()
|
|
||||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
|
||||||
|
|
||||||
_tds_accounts = frappe.get_all(
|
|
||||||
"Tax Withholding Account",
|
|
||||||
{"company": filters.get("company")},
|
|
||||||
["account", "parent"],
|
|
||||||
)
|
|
||||||
tds_accounts = {}
|
|
||||||
for tds_acc in _tds_accounts:
|
|
||||||
# if it turns out not to be the only tax withholding category, then don't include in the map
|
|
||||||
if tds_acc["account"] in tds_accounts:
|
|
||||||
tds_accounts[tds_acc["account"]] = None
|
|
||||||
else:
|
|
||||||
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
|
|
||||||
|
|
||||||
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
|
|
||||||
|
|
||||||
for d in tds_docs:
|
|
||||||
if d.voucher_type == "Purchase Invoice":
|
|
||||||
purchase_invoices.append(d.voucher_no)
|
|
||||||
if d.voucher_type == "Sales Invoice":
|
|
||||||
sales_invoices.append(d.voucher_no)
|
|
||||||
elif d.voucher_type == "Payment Entry":
|
|
||||||
payment_entries.append(d.voucher_no)
|
|
||||||
elif d.voucher_type == "Journal Entry":
|
|
||||||
journal_entries.append(d.voucher_no)
|
|
||||||
|
|
||||||
tds_documents.append(d.voucher_no)
|
|
||||||
|
|
||||||
if purchase_invoices:
|
|
||||||
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
|
|
||||||
|
|
||||||
if sales_invoices:
|
|
||||||
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
|
|
||||||
|
|
||||||
if payment_entries:
|
|
||||||
get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
|
|
||||||
|
|
||||||
if journal_entries:
|
|
||||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
|
||||||
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
|
|
||||||
|
|
||||||
return (
|
|
||||||
tds_documents,
|
|
||||||
tds_accounts,
|
|
||||||
tax_category_map,
|
|
||||||
journal_entry_party_map,
|
|
||||||
net_total_map,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
|
||||||
if not tds_accounts:
|
|
||||||
frappe.throw(
|
|
||||||
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
|
|
||||||
title=_("Accounts Missing Error"),
|
|
||||||
)
|
|
||||||
gle = frappe.qb.DocType("GL Entry")
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(gle)
|
frappe.qb.from_(twe)
|
||||||
.select("voucher_no", "voucher_type", "against", "party")
|
.select(
|
||||||
.where(gle.is_cancelled == 0)
|
twe.company,
|
||||||
|
twe.party_type,
|
||||||
|
twe.party,
|
||||||
|
IfNull(twe.tax_id, "").as_("tax_id"),
|
||||||
|
twe.tax_withholding_category,
|
||||||
|
IfNull(twe.tax_withholding_group, "").as_("tax_withholding_group"),
|
||||||
|
twe.taxable_amount,
|
||||||
|
twe.tax_rate,
|
||||||
|
twe.withholding_amount,
|
||||||
|
IfNull(twe.taxable_doctype, "").as_("taxable_doctype"),
|
||||||
|
IfNull(twe.taxable_name, "").as_("taxable_name"),
|
||||||
|
twe.taxable_date,
|
||||||
|
IfNull(twe.under_withheld_reason, "").as_("under_withheld_reason"),
|
||||||
|
IfNull(twe.lower_deduction_certificate, "").as_("lower_deduction_certificate"),
|
||||||
|
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
|
||||||
|
IfNull(twe.withholding_name, "").as_("withholding_name"),
|
||||||
|
twe.withholding_date,
|
||||||
|
twe.status,
|
||||||
|
)
|
||||||
|
.where(twe.docstatus == 1)
|
||||||
|
.where(twe.withholding_date >= filters.from_date)
|
||||||
|
.where(twe.withholding_date <= filters.to_date)
|
||||||
|
.where(IfNull(twe.withholding_name, "") != "")
|
||||||
|
.where(twe.status != "Duplicate")
|
||||||
)
|
)
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("company"):
|
||||||
query = query.where(gle.posting_date >= filters.get("from_date"))
|
query = query.where(twe.company == filters.get("company"))
|
||||||
if filters.get("to_date"):
|
|
||||||
query = query.where(gle.posting_date <= filters.get("to_date"))
|
if filters.get("party_type"):
|
||||||
|
query = query.where(twe.party_type == filters.get("party_type"))
|
||||||
|
|
||||||
if filters.get("party"):
|
if filters.get("party"):
|
||||||
party = [filters.get("party")]
|
query = query.where(twe.party == filters.get("party"))
|
||||||
jv_condition = gle.against.isin(party) | (
|
|
||||||
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
|
||||||
jv_condition = gle.against.isin(party) | (
|
|
||||||
(gle.voucher_type == "Journal Entry")
|
|
||||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
|
||||||
)
|
|
||||||
|
|
||||||
query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
return query.run(as_dict=True)
|
||||||
if bank_accounts:
|
|
||||||
query = query.where(
|
|
||||||
gle.against.notin(bank_accounts) & (gle.account.isin(tds_accounts) & jv_condition)
|
|
||||||
| gle.party.isin(party)
|
|
||||||
)
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def get_journal_entry_party_map(journal_entries):
|
def get_additional_doc_info(entries):
|
||||||
journal_entry_party_map = {}
|
"""Fetch additional document information in batch"""
|
||||||
for d in frappe.db.get_all(
|
doc_info = {}
|
||||||
"Journal Entry Account",
|
docs_by_type = {
|
||||||
{
|
"Purchase Invoice": set(),
|
||||||
"parent": ("in", journal_entries),
|
"Sales Invoice": set(),
|
||||||
"party_type": ("in", ("Supplier", "Customer")),
|
"Payment Entry": set(),
|
||||||
"party": ("is", "set"),
|
"Journal Entry": set(),
|
||||||
},
|
|
||||||
["parent", "party"],
|
|
||||||
):
|
|
||||||
if d.parent not in journal_entry_party_map:
|
|
||||||
journal_entry_party_map[d.parent] = []
|
|
||||||
journal_entry_party_map[d.parent].append(d.party)
|
|
||||||
|
|
||||||
return journal_entry_party_map
|
|
||||||
|
|
||||||
|
|
||||||
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
|
||||||
common_fields = ["name"]
|
|
||||||
fields_dict = {
|
|
||||||
"Purchase Invoice": [
|
|
||||||
"tax_withholding_category",
|
|
||||||
"base_tax_withholding_net_total",
|
|
||||||
"grand_total",
|
|
||||||
"base_total",
|
|
||||||
"bill_no",
|
|
||||||
"bill_date",
|
|
||||||
],
|
|
||||||
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
|
|
||||||
"Payment Entry": [
|
|
||||||
"tax_withholding_category",
|
|
||||||
"paid_amount",
|
|
||||||
"paid_amount_after_tax",
|
|
||||||
"base_paid_amount",
|
|
||||||
],
|
|
||||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = frappe.get_all(
|
# Group documents by type
|
||||||
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
|
for entry in entries:
|
||||||
)
|
if entry.taxable_name and entry.taxable_doctype in docs_by_type:
|
||||||
|
docs_by_type[entry.taxable_doctype].add(entry.taxable_name)
|
||||||
|
|
||||||
|
for doctype_name, voucher_set in docs_by_type.items():
|
||||||
|
if voucher_set:
|
||||||
|
_fetch_doc_info(doctype_name, voucher_set, doc_info)
|
||||||
|
|
||||||
|
return doc_info
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_doc_info(doctype_name, voucher_set, doc_info):
|
||||||
|
doctype = frappe.qb.DocType(doctype_name)
|
||||||
|
fields = [doctype.name]
|
||||||
|
|
||||||
|
# Add doctype-specific fields
|
||||||
|
if doctype_name == "Purchase Invoice":
|
||||||
|
fields.extend([doctype.grand_total, doctype.base_total, doctype.bill_no, doctype.bill_date])
|
||||||
|
elif doctype_name == "Sales Invoice":
|
||||||
|
fields.extend([doctype.grand_total, doctype.base_total])
|
||||||
|
elif doctype_name == "Payment Entry":
|
||||||
|
fields.extend(
|
||||||
|
[doctype.paid_amount_after_tax.as_("grand_total"), doctype.base_paid_amount.as_("base_total")]
|
||||||
|
)
|
||||||
|
elif doctype_name == "Journal Entry":
|
||||||
|
fields.extend([doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")])
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
|
||||||
|
entries = query.run(as_dict=True)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
|
doc_info[(doctype_name, entry.name)] = entry
|
||||||
if doctype == "Purchase Invoice":
|
|
||||||
value = [
|
|
||||||
entry.base_tax_withholding_net_total,
|
|
||||||
entry.grand_total,
|
|
||||||
entry.base_total,
|
|
||||||
entry.bill_no,
|
|
||||||
entry.bill_date,
|
|
||||||
]
|
|
||||||
elif doctype == "Sales Invoice":
|
|
||||||
value = [entry.base_net_total, entry.grand_total, entry.base_total]
|
|
||||||
elif doctype == "Payment Entry":
|
|
||||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
|
||||||
else:
|
|
||||||
value = [entry.total_debit] * 3
|
|
||||||
|
|
||||||
net_total_map[(doctype, entry.name)] = value
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_rate_map(filters):
|
|
||||||
rate_map = frappe.get_all(
|
|
||||||
"Tax Withholding Rate",
|
|
||||||
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
|
|
||||||
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
|
|
||||||
)
|
|
||||||
|
|
||||||
rate_list = frappe._dict()
|
|
||||||
|
|
||||||
for rate in rate_map:
|
|
||||||
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
|
|
||||||
|
|
||||||
return rate_list
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
|
||||||
# returns the row that matches with the fiscal year from posting date
|
|
||||||
for rate in tax_withholding:
|
|
||||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
|
||||||
return rate.tax_withholding_rate
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user