Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
741e6a7e52 📝 Add docstrings to wo-flt-issue
Docstrings generation was requested by @rtdany10.

* https://github.com/frappe/erpnext/pull/50952#issuecomment-3619564968

The following files were modified:

* `erpnext/manufacturing/doctype/work_order/work_order.py`
2025-12-06 05:39:35 +00:00
399 changed files with 64028 additions and 82033 deletions

View File

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

View File

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

View File

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

View File

@@ -21,14 +21,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.14"
python-version: "3.12"
- name: Run script to update POT file
run: |

View File

@@ -12,12 +12,12 @@ jobs:
name: linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Set up Python 3.14
uses: actions/setup-python@v6
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.14'
python-version: '3.10'
cache: pip
- name: Install and Run Pre-commit
@@ -27,12 +27,12 @@ jobs:
name: semgrep
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Set up Python 3.14
uses: actions/setup-python@v6
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.14'
python-version: '3.10'
cache: pip
- name: Download Semgrep rules

View File

@@ -29,7 +29,7 @@ jobs:
services:
mysql:
image: mariadb:11.8
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Check for valid Python & Merge Conflicts
run: |
@@ -49,17 +49,14 @@ jobs:
fi
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: |
3.11
3.13
3.14
python-version: '3.11'
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 18
check-latest: true
- name: Add to Hosts
@@ -135,15 +132,15 @@ jobs:
# Resetup env and install apps
pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
bench -v setup env --python python$2
bench -v setup env
bench pip install -e ./apps/erpnext
bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
}
update_to_version 14 3.11
update_to_version 15 3.13
update_to_version 14
update_to_version 15
echo "Updating to latest version"
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"

View File

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

View File

@@ -17,7 +17,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- id: set-matrix
run: |
# Use grep and find to get the list of test files
@@ -72,17 +72,17 @@ jobs:
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: '3.14'
python-version: '3.12'
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 18
check-latest: true
- name: Add to Hosts

View File

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

View File

@@ -62,12 +62,12 @@ jobs:
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: '3.14'
python-version: '3.12'
- name: Check for valid Python & Merge Conflicts
run: |
@@ -78,9 +78,9 @@ jobs:
fi
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 18
check-latest: true
- name: Add to Hosts
@@ -149,7 +149,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4

View File

@@ -47,12 +47,12 @@ jobs:
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: '3.14'
python-version: '3.12'
- name: Check for valid Python & Merge Conflicts
run: |
@@ -63,9 +63,9 @@ jobs:
fi
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 18
check-latest: true
- name: Add to Hosts

View File

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

View File

@@ -70,7 +70,6 @@ frappe.treeview_settings["Account"] = {
args: {
accounts: accounts,
company: cur_tree.args.company,
include_default_fb_balances: true,
},
});

View File

@@ -12,7 +12,6 @@
"column_break_4",
"company",
"disabled",
"exempted_role",
"section_break_7",
"closed_documents"
],
@@ -68,18 +67,10 @@
"label": "Closed Documents",
"options": "Closed Document",
"reqd": 1
},
{
"description": "Role allowed to bypass period restrictions.",
"fieldname": "exempted_role",
"fieldtype": "Link",
"label": "Exempted Role",
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
"options": "Role"
}
],
"links": [],
"modified": "2025-12-01 16:53:44.631299",
"modified": "2025-10-06 15:00:15.568067",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",

View File

@@ -30,7 +30,6 @@ class AccountingPeriod(Document):
company: DF.Link
disabled: DF.Check
end_date: DF.Date
exempted_role: DF.Link | None
period_name: DF.Data
start_date: DF.Date
# end: auto-generated types
@@ -114,7 +113,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name, ap.exempted_role)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
@@ -127,11 +126,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
).run(as_dict=1)
if accounting_period:
if (
accounting_period[0].get("exempted_role")
and accounting_period[0].get("exempted_role") in frappe.get_roles()
):
return
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])

View File

@@ -152,5 +152,6 @@ class AccountsSettings(Document):
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

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

View File

@@ -0,0 +1,27 @@
# 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

View File

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

View File

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

View File

@@ -125,7 +125,7 @@ class BankClearance(Document):
)
msg += "</ul>"
msgprint(_(msg))
frappe.throw(_(msg))
return
if not entries_to_update:

View File

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

View File

@@ -304,7 +304,6 @@ def create_payment_entry_bts(
project=None,
cost_center=None,
allow_edit=None,
company_bank_account=None,
):
# Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values(
@@ -346,9 +345,6 @@ def create_payment_entry_bts(
pe.project = project
pe.cost_center = cost_center
if company_bank_account:
pe.bank_account = company_bank_account
pe.validate()
if allow_edit:

View File

@@ -38,10 +38,7 @@
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban",
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
"bank_party_iban"
],
"fields": [
{
@@ -236,32 +233,12 @@
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-12-07 20:49:18.600757",
"modified": "2025-10-23 17:32:58.514807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -32,8 +32,6 @@ class BankTransaction(Document):
date: DF.Date | None
deposit: DF.Currency
description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None
party_type: DF.Link | None
@@ -47,14 +45,9 @@ class BankTransaction(Document):
# end: auto-generated types
def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount()
def on_discard(self):
self.db_set("status", "Cancelled")
def validate(self):
self.validate_included_fee()
self.validate_duplicate_references()
self.validate_currency()
@@ -314,40 +307,6 @@ class BankTransaction(Document):
self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():

View File

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

View File

@@ -12,15 +12,6 @@ frappe.ui.form.on("Budget", {
};
});
frm.set_query("account", function () {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
if (value) {
@@ -33,16 +24,24 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
let exception_role = await frappe.db.get_value(
"Company",
frm.doc.company,
"exception_budget_approver_role"
);
}
toggle_distribution_fields(frm);
const role = exception_role.message.exception_budget_approver_role;
if (role && frappe.user.has_role(role)) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
}
}
},
budget_against: function (frm) {
@@ -55,15 +54,10 @@ frappe.ui.form.on("Budget", {
frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
});
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
distribute_equally: function (frm) {
toggle_distribution_fields(frm);
},
set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null);
@@ -106,8 +100,6 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
@@ -115,29 +107,7 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
});
function set_total_budget_amount(frm) {
let total = 0;
(frm.doc.budget_distribution || []).forEach((row) => {
total += flt(row.amount);
});
frm.set_value("budget_distribution_total", total);
}
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});
grid.refresh();
}

View File

@@ -25,10 +25,6 @@
"distribute_equally",
"section_break_fpdt",
"budget_distribution",
"section_break_wkqb",
"column_break_paum",
"column_break_nwor",
"budget_distribution_total",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
@@ -226,8 +222,7 @@
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break",
"hide_border": 1
"fieldtype": "Section Break"
},
{
"fieldname": "budget_distribution",
@@ -308,32 +303,13 @@
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "section_break_wkqb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_paum",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_nwor",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_distribution_total",
"fieldtype": "Currency",
"label": "Budget Distribution Total",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-12-10 02:35:01.197613",
"modified": "2025-11-19 17:00:00.648224",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -53,7 +53,6 @@ class Budget(Document):
budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_distribution_total: DF.Currency
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link
@@ -231,49 +230,28 @@ class Budget(Document):
def before_save(self):
self.allocate_budget()
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self._should_skip_allocation():
return
if self._should_recalculate_manual_distribution():
self._recalculate_manual_distribution()
if self.revision_of:
return
if not self.should_regenerate_budget_distribution():
return
self._regenerate_distribution()
self.set("budget_distribution", [])
def _should_skip_allocation(self):
return self.revision_of and not self.distribute_equally
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
def _should_recalculate_manual_distribution(self):
return (
not self.distribute_equally
and bool(self.budget_distribution)
and self._is_only_budget_amount_changed()
)
def _is_only_budget_amount_changed(self):
old = self.get_doc_before_save()
if not old:
return False
return (
old.budget_amount != self.budget_amount
and old.distribution_frequency == self.distribution_frequency
and old.budget_start_date == self.budget_start_date
and old.budget_end_date == self.budget_end_date
)
def _recalculate_manual_distribution(self):
for row in self.budget_distribution:
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated."""
@@ -287,6 +265,7 @@ class Budget(Document):
"to_fiscal_year",
"budget_amount",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
@@ -294,21 +273,6 @@ class Budget(Document):
return bool(self.distribute_equally)
def _regenerate_distribution(self):
self.set("budget_distribution", [])
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
self.budget_distribution_total = self.budget_amount
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency
@@ -348,8 +312,12 @@ class Budget(Document):
}.get(frequency, 1)
def add_allocated_amount(self, row, row_percent):
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
if not self.distribute_equally:
row.amount = 0
row.percent = 0
else:
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution():

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
} else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
elif self.service_provider == "frankfurter.dev":
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,9 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"

View File

@@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = {
return [__("Disbursed"), "blue", "status,=,Disbursed"];
} else if (doc.status == "Settled") {
return [__("Settled"), "orange", "status,=,Settled"];
} else if (doc.status == "Cancelled") {
return [__("Cancelled"), "red", "status,=,Cancelled"];
} else if (doc.status == "Canceled") {
return [__("Canceled"), "red", "status,=,Canceled"];
}
},
};

View File

@@ -201,7 +201,6 @@ frappe.ui.form.on("Journal Entry", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
frm.clear_table("tax_withholding_entries");
},
voucher_type: function (frm) {
@@ -252,10 +251,6 @@ frappe.ui.form.on("Journal Entry", {
});
}
},
apply_tds: function (frm) {
frm.clear_table("tax_withholding_entries");
},
});
var update_jv_details = function (doc, r) {

View File

@@ -43,11 +43,6 @@
"total_amount_currency",
"total_amount",
"total_amount_in_words",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"reference",
"clearance_date",
"remark",
@@ -522,7 +517,7 @@
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Consider for Tax Withholding "
"label": "Apply Tax Withholding Amount "
},
{
"depends_on": "eval:doc.docstatus",
@@ -591,39 +586,6 @@
"hidden": 1,
"label": "Party Not Required",
"no_copy": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
"depends_on": "eval: doc.apply_tds",
"fieldname": "section_tax_withholding_entry",
"fieldtype": "Section Break",
"label": "Tax Withholding Entry"
},
{
"fieldname": "tax_withholding_group",
"fieldtype": "Link",
"label": "Tax Withholding Group",
"options": "Tax Withholding Group"
},
{
"default": "0",
"fieldname": "ignore_tax_withholding_threshold",
"fieldtype": "Check",
"label": "Ignore Tax Withholding Threshold"
},
{
"default": "0",
"fieldname": "override_tax_withholding_entries",
"fieldtype": "Check",
"label": "Edit Tax Withholding Entries"
},
{
"fieldname": "tax_withholding_entries",
"fieldtype": "Table",
"label": "Tax Withholding Entries",
"options": "Tax Withholding Entry",
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
}
],
"icon": "fa fa-file-text",
@@ -638,7 +600,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-11-13 17:54:14.542903",
"modified": "2025-09-29 13:05:46.982277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -17,7 +17,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -31,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -48,7 +49,6 @@ class JournalEntry(AccountsController):
from frappe.types import DF
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
accounts: DF.Table[JournalEntryAccount]
amended_from: DF.Link | None
@@ -65,7 +65,6 @@ class JournalEntry(AccountsController):
finance_book: DF.Link | None
for_all_stock_asset_accounts: DF.Check
from_template: DF.Link | None
ignore_tax_withholding_threshold: DF.Check
inter_company_journal_entry_reference: DF.Link | None
is_opening: DF.Literal["No", "Yes"]
is_system_generated: DF.Check
@@ -74,7 +73,6 @@ class JournalEntry(AccountsController):
multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
party_not_required: DF.Check
override_tax_withholding_entries: DF.Check
pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None
periodic_entry_difference_account: DF.Link | None
@@ -86,8 +84,6 @@ class JournalEntry(AccountsController):
stock_asset_account: DF.Link | None
stock_entry: DF.Link | None
tax_withholding_category: DF.Link | None
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
tax_withholding_group: DF.Link | None
title: DF.Data | None
total_amount: DF.Currency
total_amount_currency: DF.Link | None
@@ -154,8 +150,8 @@ class JournalEntry(AccountsController):
self.validate_company_in_accounting_dimension()
self.validate_advance_accounts()
JournalTaxWithholding(self).on_validate()
if self.docstatus == 0:
self.apply_tax_withholding()
if self.is_new() or not self.title:
self.title = self.get_title()
@@ -203,7 +199,6 @@ class JournalEntry(AccountsController):
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
JournalTaxWithholding(self).on_submit()
@frappe.whitelist()
def get_balance_for_periodic_accounting(self):
@@ -287,8 +282,6 @@ class JournalEntry(AccountsController):
self.repost_accounting_entries()
def on_cancel(self):
# Cancel tax withholding entries
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super().on_cancel()
self.ignore_linked_doctypes = (
@@ -302,10 +295,8 @@ class JournalEntry(AccountsController):
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
"Tax Withholding Entry",
)
self.make_gl_entries(1)
JournalTaxWithholding(self).on_cancel()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@@ -361,6 +352,95 @@ class JournalEntry(AccountsController):
StockAccountInvalidTransaction,
)
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
debit_or_credit = (
"debit_in_account_currency"
if self.voucher_type == "Credit Note"
else "credit_in_account_currency"
)
rev_debit_or_credit = (
"credit_in_account_currency"
if debit_or_credit == "debit_in_account_currency"
else "debit_in_account_currency"
)
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(
d.get(debit_or_credit)
for d in self.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
)
party_amount = sum(
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
)
inv = frappe._dict(
{
party_type: parties[0],
"doctype": doctype,
"company": self.company,
"posting_date": self.posting_date,
"net_total": net_total,
}
)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
accounts = []
for d in self.get("accounts"):
if d.get("account") == tax_withholding_details.get("account_head"):
d.update(
{
"account": tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get("tax_amount"),
}
)
accounts.append(d.get("account"))
if d.get("account") == party_account:
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append(
"accounts",
{
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
},
)
to_remove = [
d
for d in self.get("accounts")
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
]
for d in to_remove:
self.remove(d)
def update_asset_value(self):
self.update_asset_on_depreciation()
self.update_asset_on_disposal()
@@ -1674,6 +1754,8 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
@@ -1696,7 +1778,7 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -34,7 +34,6 @@
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"is_tax_withholding_account",
"col_break3",
"is_advance",
"user_remark",
@@ -282,19 +281,12 @@
"options": "advance_voucher_type",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-27 12:23:33.157655",
"modified": "2025-10-27 13:48:32.805100",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
debit_in_account_currency: DF.Currency
exchange_rate: DF.Float
is_advance: DF.Literal["No", "Yes"]
is_tax_withholding_account: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -214,9 +214,6 @@ class OpeningInvoiceCreationTool(Document):
}
)
if self.invoice_type == "Purchase" and row.supplier_invoice_date:
invoice.update({"bill_date": row.supplier_invoice_date})
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({dimension: self.get(dimension) or item.get(dimension)})

View File

@@ -12,7 +12,6 @@
"column_break_3",
"posting_date",
"due_date",
"supplier_invoice_date",
"section_break_5",
"item_name",
"outstanding_amount",
@@ -112,26 +111,19 @@
"fieldname": "invoice_number",
"fieldtype": "Data",
"label": "Invoice Number"
},
{
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"label": "Supplier Invoice Date"
}
],
"istable": 1,
"links": [],
"modified": "2025-12-01 16:18:07.997594",
"modified": "2024-03-27 13:10:06.703006",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -26,7 +26,6 @@ class OpeningInvoiceCreationToolItem(Document):
party_type: DF.Link | None
posting_date: DF.Date | None
qty: DF.Data | None
supplier_invoice_date: DF.Date | None
temporary_opening_account: DF.Link | None
# end: auto-generated types

View File

@@ -41,7 +41,6 @@ frappe.ui.form.on("Payment Entry", {
if (frm.is_new()) {
set_default_party_type(frm);
frm.clear_table("tax_withholding_entries");
}
},
@@ -427,15 +426,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Internal Transfer") {
$.each(
[
"party",
"party_type",
"paid_from",
"paid_to",
"references",
"total_allocated_amount",
"party_name",
],
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
function (i, field) {
frm.set_value(field, null);
}
@@ -541,7 +532,6 @@ frappe.ui.form.on("Payment Entry", {
},
() => frm.set_value("party_name", r.message.party_name),
() => frm.clear_table("references"),
() => frm.clear_table("tax_withholding_entries"),
() => frm.events.hide_unhide_fields(frm),
() => frm.events.set_dynamic_labels(frm),
() => {
@@ -574,15 +564,14 @@ frappe.ui.form.on("Payment Entry", {
}
},
apply_tds: function (frm) {
if (!frm.doc.apply_tds) {
apply_tax_withholding_amount: function (frm) {
if (!frm.doc.apply_tax_withholding_amount) {
frm.set_value("tax_withholding_category", "");
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
} else {
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
frm.set_value("tax_withholding_category", values.tax_withholding_category);
});
}
frm.clear_table("tax_withholding_entries");
},
paid_from: function (frm) {
@@ -1288,14 +1277,15 @@ frappe.ui.form.on("Payment Entry", {
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
const response = await get_company_defaults(frm.doc.company);
const account =
company_defaults?.[account_fieldname] ||
response.message?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = company_defaults?.cost_center;
row.cost_center = response.message?.cost_center;
row.is_exchange_gain_loss = 1;
}
@@ -1505,14 +1495,18 @@ frappe.ui.form.on("Payment Entry", {
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
);
d.row_id = "";
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
} else if (
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
d.row_id
) {
if (d.idx == 1) {
msg = __(
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
);
d.charge_type = "";
} else if (!d.row_id) {
d.row_id = d.idx - 1;
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
d.row_id = "";
} else if (d.row_id && d.row_id >= d.idx) {
msg = __(
"Cannot refer row number greater than or equal to current row number for this Charge type"

View File

@@ -21,8 +21,6 @@
"party_name",
"book_advance_payments_in_separate_party_account",
"reconcile_on_advance_payment_date",
"apply_tds",
"tax_withholding_category",
"column_break_11",
"bank_account",
"party_bank_account",
@@ -62,6 +60,10 @@
"taxes_and_charges_section",
"purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template",
"column_break_55",
"apply_tax_withholding_amount",
"tax_withholding_category",
"section_break_56",
"taxes",
"section_break_60",
"base_total_taxes_and_charges",
@@ -69,11 +71,6 @@
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"transaction_references",
"reference_no",
"column_break_23",
@@ -581,17 +578,24 @@
"label": "Custom Remarks"
},
{
"depends_on": "eval:doc.apply_tds",
"depends_on": "eval:doc.apply_tax_withholding_amount",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"mandatory_depends_on": "eval:doc.apply_tds",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"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",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Taxes and Charges"
},
{
@@ -644,6 +648,15 @@
"options": "Company:company:default_currency",
"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'",
"fieldname": "received_amount_after_tax",
@@ -682,7 +695,8 @@
},
{
"fieldname": "section_break_60",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "eval:doc.docstatus==0",
@@ -739,46 +753,6 @@
"options": "No\nYes",
"print_hide": 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,
@@ -793,7 +767,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-12-18 13:56:40.206038",
"modified": "2025-05-08 11:18:10.238085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -30,7 +30,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PaymentTaxWithholding
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import (
make_gl_entries,
make_reverse_gl_entries,
@@ -78,10 +80,9 @@ class PaymentEntry(AccountsController):
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
PaymentEntryReference,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
amended_from: DF.Link | None
apply_tds: DF.Check
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
@@ -102,13 +103,11 @@ class PaymentEntry(AccountsController):
custom_remarks: DF.Check
deductions: DF.Table[PaymentEntryDeduction]
difference_amount: DF.Currency
ignore_tax_withholding_threshold: DF.Check
in_words: DF.SmallText | None
is_opening: DF.Literal["No", "Yes"]
letter_head: DF.Link | None
mode_of_payment: DF.Link | None
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
override_tax_withholding_entries: DF.Check
paid_amount: DF.Currency
paid_amount_after_tax: DF.Currency
paid_from: DF.Link
@@ -140,8 +139,6 @@ class PaymentEntry(AccountsController):
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
target_exchange_rate: DF.Float
tax_withholding_category: DF.Link | None
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
tax_withholding_group: DF.Link | None
taxes: DF.Table[AdvanceTaxesandCharges]
title: DF.Data | None
total_allocated_amount: DF.Currency
@@ -192,7 +189,7 @@ class PaymentEntry(AccountsController):
self.validate_allocated_amount()
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
PaymentTaxWithholding(self).on_validate()
self.set_tax_withholding()
self.set_status()
self.set_total_in_words()
@@ -202,7 +199,6 @@ class PaymentEntry(AccountsController):
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
PaymentTaxWithholding(self).on_submit()
self.update_payment_requests()
self.update_payment_schedule()
self.make_gl_entries()
@@ -304,10 +300,8 @@ class PaymentEntry(AccountsController):
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
"Tax Withholding Entry",
)
super().on_cancel()
PaymentTaxWithholding(self).on_cancel()
self.update_payment_requests(cancel=True)
self.update_payment_schedule(cancel=1)
self.make_gl_entries(cancel=1)
@@ -943,6 +937,93 @@ class PaymentEntry(AccountsController):
self.base_in_words = money_in_words(base_amount, self.company_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):
self.initialize_taxes()
self.determine_exclusive_rate()

View File

@@ -59,15 +59,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-13 06:52:46.130142",
"modified": "2024-11-05 16:07:47.307971",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -38,7 +38,7 @@
"search_index": 1
},
{
"columns": 4,
"columns": 2,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_global_search": 1,
@@ -49,10 +49,8 @@
"search_index": 1
},
{
"columns": 2,
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date",
"read_only": 1
},
@@ -176,7 +174,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-08 13:57:30.098239",
"modified": "2025-07-25 04:32:11.040025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -100,10 +100,7 @@ class PaymentRequest(Document):
subscription_plans: DF.Table[SubscriptionPlanDetail]
swift_number: DF.ReadOnly | None
transaction_date: DF.Date | None
# end: auto-generated types
def on_discard(self):
self.db_set("status", "Cancelled")
def validate(self):
if self.get("__islocal"):
@@ -430,7 +427,6 @@ class PaymentRequest(Document):
context = {
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
"payment_url": self.payment_url,
"payment_request": self,
}
if self.message:
@@ -543,9 +539,6 @@ def make_payment_request(**args):
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
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)
if not args.get("company"):
args.company = ref_doc.company
@@ -850,7 +843,6 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
doc_updates = {}
for ref in references:
if not ref.payment_request:
@@ -876,7 +868,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
title=_("Invalid Allocated Amount"),
)
# determine status
# update status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
@@ -884,37 +876,31 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
elif new_outstanding_amount > 0:
status = "Partially Paid"
# prepare bulk update data
doc_updates[ref.payment_request] = {
"outstanding_amount": new_outstanding_amount,
"status": status,
}
# bulk update all payment requests
if doc_updates:
frappe.db.bulk_update("Payment Request", doc_updates)
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
def get_dummy_message(doc):
return """
{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %}
return frappe.render_template(
"""{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %}
<p>
{{ _("Requesting payment against {0} {1} for amount {2}").format(
doc.doctype,
doc.name,
payment_request.get_formatted("grand_total")
) }}
</p>
<p>{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype,
doc.name, doc.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()

View File

@@ -35,10 +35,7 @@ class ProcessPaymentReconciliation(Document):
]
to_invoice_date: DF.Date | None
to_payment_date: DF.Date | None
# end: auto-generated types
def on_discard(self):
self.db_set("status", "Cancelled")
def validate(self):
self.validate_receivable_payable_account()

View File

@@ -36,10 +36,7 @@ class ProcessPeriodClosingVoucher(Document):
parent_pcv: DF.Link
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
# end: auto-generated types
def on_discard(self):
self.db_set("status", "Cancelled")
def validate(self):
self.status = "Queued"

View File

@@ -130,7 +130,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -224,6 +224,7 @@ 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);
}
@@ -363,9 +364,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
},
function () {
me.apply_pricing_rule();
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.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
// while duplicating, don't change payment terms
if (me.frm.doc.__run_link_triggers === false) {
@@ -378,7 +380,26 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
apply_tds(frm) {
var me = this;
me.frm.clear_table("tax_withholding_entries");
me.frm.set_value("tax_withheld_vouchers", []);
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() {
@@ -558,6 +579,17 @@ 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) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
@@ -671,7 +703,10 @@ frappe.ui.form.on("Purchase Invoice", {
onload: function (frm) {
if (frm.doc.__onload && frm.doc.supplier) {
if (frm.is_new()) {
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
if (!frm.doc.__onload.supplier_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}
@@ -680,7 +715,7 @@ frappe.ui.form.on("Purchase Invoice", {
});
if (frm.is_new()) {
frm.clear_table("tax_withholding_entries");
frm.clear_table("tax_withheld_vouchers");
}
},
@@ -707,7 +742,6 @@ frappe.ui.form.on("Purchase Invoice", {
company: function (frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
frm.clear_table("tax_withholding_entries");
if (frm.doc.company) {
frappe.call({

View File

@@ -27,6 +27,7 @@
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"tax_withholding_category",
"amended_from",
"supplier_invoice_details",
"bill_no",
@@ -67,6 +68,8 @@
"column_break_28",
"total",
"net_total",
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
@@ -99,17 +102,14 @@
"total_advance",
"outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"section_break_44",
"apply_discount_on",
"base_discount_amount",
"column_break_46",
"additional_discount_percentage",
"discount_amount",
"tax_withheld_vouchers_section",
"tax_withheld_vouchers",
"sec_tax_breakup",
"other_charges_calculation",
"item_wise_tax_details",
@@ -130,6 +130,7 @@
"only_include_allocated_payments",
"get_advances",
"advances",
"advance_tax",
"write_off",
"write_off_amount",
"base_write_off_amount",
@@ -285,7 +286,7 @@
"default": "0",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Consider for Tax Withholding",
"label": "Apply Tax Withholding Amount",
"print_hide": 1
},
{
@@ -1357,6 +1358,14 @@
"fieldname": "dimension_col_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",
"fieldtype": "Link",
@@ -1446,6 +1455,14 @@
"fieldname": "column_break_147",
"fieldtype": "Column Break"
},
{
"fieldname": "advance_tax",
"fieldtype": "Table",
"hidden": 1,
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
},
{
"fieldname": "subscription",
"fieldtype": "Link",
@@ -1460,6 +1477,42 @@
"label": "Is Old Subcontracting Flow",
"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",
"fieldtype": "Tab Break",
@@ -1609,7 +1662,7 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
},
{
"fieldname": "claimed_landed_cost_amount",
"fieldtype": "Currency",
@@ -1626,40 +1679,6 @@
"label": "Item Wise Tax Details",
"no_copy": 1,
"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,
@@ -1667,7 +1686,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-12-15 06:41:38.237728",
"modified": "2025-08-04 19:19:11.380664",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -24,7 +24,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_linked_doc,
validate_inter_company_party,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PurchaseTaxWithholding
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import (
get_round_off_account_and_cost_center,
make_gl_entries,
@@ -59,6 +61,7 @@ class PurchaseInvoice(BuyingController):
if TYPE_CHECKING:
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.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
@@ -69,13 +72,14 @@ class PurchaseInvoice(BuyingController):
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
PurchaseTaxesandCharges,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
PurchaseReceiptItemSupplied,
)
additional_discount_percentage: DF.Float
address_display: DF.TextEditor | None
advance_tax: DF.Table[AdvanceTax]
advances: DF.Table[PurchaseInvoiceAdvance]
against_expense_account: DF.SmallText | None
allocate_advances_automatically: DF.Check
@@ -90,6 +94,7 @@ class PurchaseInvoice(BuyingController):
base_paid_amount: DF.Currency
base_rounded_total: 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_deducted: DF.Currency
base_total: DF.Currency
@@ -123,7 +128,6 @@ class PurchaseInvoice(BuyingController):
hold_comment: DF.SmallText | None
ignore_default_payment_terms_template: DF.Check
ignore_pricing_rule: DF.Check
ignore_tax_withholding_threshold: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None
inter_company_invoice_reference: DF.Link | None
@@ -145,7 +149,6 @@ class PurchaseInvoice(BuyingController):
only_include_allocated_payments: DF.Check
other_charges_calculation: DF.TextEditor | None
outstanding_amount: DF.Currency
override_tax_withholding_entries: DF.Check
paid_amount: DF.Currency
party_account_currency: DF.Link | None
payment_schedule: DF.Table[PaymentSchedule]
@@ -195,8 +198,9 @@ class PurchaseInvoice(BuyingController):
supplier_warehouse: DF.Link | None
tax_category: DF.Link | None
tax_id: DF.ReadOnly | None
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
tax_withholding_group: DF.Link | None
tax_withheld_vouchers: DF.Table[TaxWithheldVouchers]
tax_withholding_category: DF.Link | None
tax_withholding_net_total: DF.Currency
taxes: DF.Table[PurchaseTaxesandCharges]
taxes_and_charges: DF.Link | None
taxes_and_charges_added: DF.Currency
@@ -241,14 +245,11 @@ class PurchaseInvoice(BuyingController):
def onload(self):
super().onload()
if self.supplier:
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)
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
if self.is_new():
self.set("tax_withholding_entries", [])
self.set("tax_withheld_vouchers", [])
def before_save(self):
if not self.on_hold:
@@ -299,7 +300,6 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
PurchaseTaxWithholding(self).on_validate()
self.set_percentage_received()
def set_percentage_received(self):
@@ -352,13 +352,11 @@ class PurchaseInvoice(BuyingController):
template_name=self.payment_terms_template,
)
if self.supplier:
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
"Supplier", self.supplier, ["tax_withholding_category", "tax_withholding_group"]
)
if not for_validate:
if tax_withholding_category or tax_withholding_group:
self.apply_tds = 1
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.apply_tds = 1
self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
super().set_missing_values(for_validate)
@@ -749,7 +747,6 @@ class PurchaseInvoice(BuyingController):
def on_submit(self):
super().on_submit()
PurchaseTaxWithholding(self).on_submit()
self.check_prev_docstatus()
@@ -791,6 +788,7 @@ class PurchaseInvoice(BuyingController):
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references()
self.process_common_party_accounting()
@@ -1674,7 +1672,6 @@ class PurchaseInvoice(BuyingController):
check_if_return_invoice_linked_with_payment_entry(self)
super().on_cancel()
PurchaseTaxWithholding(self).on_cancel()
self.check_on_hold_or_closed_status()
@@ -1721,9 +1718,10 @@ class PurchaseInvoice(BuyingController):
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
"Serial and Batch Bundle",
"Tax Withholding Entry",
)
self.update_advance_tax_references(cancel=1)
def update_project(self):
projects = frappe._dict()
@@ -1846,6 +1844,102 @@ class PurchaseInvoice(BuyingController):
self.db_set("on_hold", 0)
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):
if self.is_new():
if self.get("amended_from"):
@@ -1933,7 +2027,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Purchase Invoices"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context
@@ -2004,26 +2097,10 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
if isinstance(args, str):
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):
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.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(returned_qty_map.get("qty"))) * flt(
obj.conversion_factor
)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
@@ -2062,7 +2139,6 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
},
target_doc,
post_parent_process,
)
return doc

View File

@@ -1538,7 +1538,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
# Create Payment Entry Against the order
payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
payment_entry.paid_from = "Cash - _TC"
payment_entry.apply_tds = 1
payment_entry.apply_tax_withholding_amount = 1
payment_entry.tax_withholding_category = tax_withholding_category
payment_entry.save()
payment_entry.submit()
@@ -1591,26 +1591,12 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
self.assertEqual(expected_gle[i][1], gle.amount)
payment_entry.load_from_db()
tax_allocated = sum(
[
entry.withholding_amount
for entry in payment_entry.get("tax_withholding_entries", [])
if entry.taxable_name
]
)
self.assertEqual(tax_allocated, 3000)
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
purchase_invoice.cancel()
payment_entry.load_from_db()
tax_allocated = sum(
[
entry.withholding_amount
for entry in payment_entry.get("tax_withholding_entries", [])
if entry.taxable_name
]
)
self.assertEqual(tax_allocated, 0)
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_purchase_gl_with_tax_withholding_tax(self):
company = "_Test Company"
@@ -1645,6 +1631,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
do_not_submit=1,
)
pi.apply_tds = 1
pi.tax_withholding_category = tax_withholding_category
pi.save()
pi.submit()
@@ -2944,29 +2931,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
pi.save()
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):
frappe.db.set_value(

View File

@@ -44,7 +44,6 @@
"rate",
"amount",
"item_tax_template",
"tax_withholding_category",
"col_break4",
"base_rate",
"base_amount",
@@ -229,6 +228,7 @@
"reqd": 1
},
{
"default": "1",
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
@@ -893,7 +893,7 @@
"default": "1",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Consider for Tax Withholding"
"label": "Apply TDS"
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
@@ -979,20 +979,13 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"print_hide": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-12-13 14:10:02.379392",
"modified": "2025-10-14 13:00:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -90,7 +90,6 @@ class PurchaseInvoiceItem(Document):
stock_qty: DF.Float
stock_uom: DF.Link | None
stock_uom_rate: DF.Currency
tax_withholding_category: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check

View File

@@ -34,8 +34,7 @@
"base_net_amount",
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
"dont_recompute_tax"
"base_tax_amount_after_discount_amount"
],
"fields": [
{
@@ -206,11 +205,11 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
@@ -263,22 +262,13 @@
"print_hide": 1,
"read_only": 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,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-24 18:22:56.886010",
"modified": "2025-07-24 15:08:44.433022",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -32,7 +32,6 @@ class PurchaseTaxesandCharges(Document):
]
cost_center: DF.Link | None
description: DF.SmallText
dont_recompute_tax: DF.Check
included_in_paid_amount: DF.Check
included_in_print_rate: DF.Check
is_tax_withholding_account: DF.Check
@@ -40,7 +39,6 @@ class PurchaseTaxesandCharges(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
project: DF.Link | None
rate: DF.Float
row_id: DF.Data | None
set_by_item_tax_template: DF.Check

View File

@@ -115,10 +115,6 @@ class RepostAccountingLedger(Document):
def generate_preview(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
if not self.vouchers:
frappe.msgprint(_("Add vouchers to generate preview."))
return
gl_columns = []
gl_data = []
@@ -146,7 +142,6 @@ class RepostAccountingLedger(Document):
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
)
frappe.msgprint(_("Repost has started in the background"))
else:

View File

@@ -25,7 +25,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
company() {
super.company();
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
this.frm.clear_table("tax_withholding_entries");
}
onload() {
var me = this;
@@ -383,9 +382,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
),
},
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();
}
);
@@ -602,10 +598,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.calculate_taxes_and_totals();
}
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
};
// for backward compatibility: combine new and previous states
@@ -626,6 +618,10 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
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", {
setup: function (frm) {
frm.add_fetch("customer", "tax_id", "tax_id");
@@ -822,16 +818,6 @@ frappe.ui.form.on("Sales Invoice", {
},
onload: function (frm) {
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) {

View File

@@ -28,7 +28,6 @@
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"is_debit_note",
"apply_tds",
"amended_from",
"is_created_using_pos",
"pos_closing_entry",
@@ -91,11 +90,6 @@
"total_advance",
"outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"section_break_49",
"apply_discount_on",
"base_discount_amount",
@@ -2253,46 +2247,6 @@
"label": "Item Wise Tax Details",
"no_copy": 1,
"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,

View File

@@ -26,7 +26,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
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.utils import (
@@ -75,7 +77,6 @@ class SalesInvoice(SellingController):
from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import (
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.stock.doctype.packed_item.packed_item import PackedItem
@@ -89,7 +90,6 @@ class SalesInvoice(SellingController):
amended_from: DF.Link | None
amount_eligible_for_commission: DF.Currency
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
apply_tds: DF.Check
auto_repeat: DF.Link | None
base_change_amount: DF.Currency
base_discount_amount: DF.Currency
@@ -135,7 +135,6 @@ class SalesInvoice(SellingController):
has_subcontracted: DF.Check
ignore_default_payment_terms_template: DF.Check
ignore_pricing_rule: DF.Check
ignore_tax_withholding_threshold: DF.Check
in_words: DF.SmallText | None
incoterm: DF.Link | None
inter_company_invoice_reference: DF.Link | None
@@ -163,7 +162,6 @@ class SalesInvoice(SellingController):
only_include_allocated_payments: DF.Check
other_charges_calculation: DF.TextEditor | None
outstanding_amount: DF.Currency
override_tax_withholding_entries: DF.Check
packed_items: DF.Table[PackedItem]
paid_amount: DF.Currency
party_account_currency: DF.Link | None
@@ -216,8 +214,6 @@ class SalesInvoice(SellingController):
subscription: DF.Link | None
tax_category: DF.Link | None
tax_id: DF.Data | None
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
tax_withholding_group: DF.Link | None
taxes: DF.Table[SalesTaxesandCharges]
taxes_and_charges: DF.Link | None
tc_name: DF.Link | None
@@ -286,14 +282,6 @@ class SalesInvoice(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
def onload(self):
super().onload()
if self.customer:
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
"Customer", self.customer, ["tax_withholding_category", "tax_withholding_group"]
)
self.set_onload("apply_tds", tax_withholding_category or tax_withholding_group)
def validate(self):
self.validate_auto_set_posting_time()
super().validate()
@@ -303,7 +291,7 @@ class SalesInvoice(SellingController):
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
SalesTaxWithholding(self).on_validate()
self.set_tax_withholding()
self.validate_proj_cust()
self.validate_pos_return()
@@ -425,6 +413,38 @@ class SalesInvoice(SellingController):
for item in self.get("items"):
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):
self.set_account_for_mode_of_payment()
self.set_paid_amount()
@@ -446,8 +466,6 @@ class SalesInvoice(SellingController):
# NOTE status updating bypassed for is_return
self.status_updater = []
SalesTaxWithholding(self).on_submit()
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -587,7 +605,6 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
SalesTaxWithholding(self).on_cancel()
if self.update_stock == 1:
self.update_stock_ledger()
@@ -629,7 +646,6 @@ class SalesInvoice(SellingController):
"Unreconcile Payment Entries",
"Payment Ledger Entry",
"Serial and Batch Bundle",
"Tax Withholding Entry",
)
self.delete_auto_created_batches()
@@ -2310,7 +2326,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Invoices"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context

View File

@@ -43,14 +43,12 @@
"rate",
"amount",
"item_tax_template",
"tax_withholding_category",
"col_break3",
"base_rate",
"base_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"apply_tds",
"grant_commission",
"section_break_21",
"net_rate",
@@ -988,21 +986,6 @@
"hidden": 1,
"label": "SCIO Detail",
"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,

View File

@@ -24,7 +24,6 @@ class SalesInvoiceItem(Document):
actual_qty: DF.Float
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
apply_tds: DF.Check
asset: DF.Link | None
barcode: DF.Data | None
base_amount: DF.Currency
@@ -96,7 +95,6 @@ class SalesInvoiceItem(Document):
stock_uom: DF.Link | None
stock_uom_rate: DF.Currency
target_warehouse: DF.Link | None
tax_withholding_category: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check

View File

@@ -14,7 +14,6 @@
"included_in_print_rate",
"included_in_paid_amount",
"set_by_item_tax_template",
"is_tax_withholding_account",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -203,7 +202,7 @@
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Don't Recompute Tax",
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
},
@@ -242,13 +241,6 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,

View File

@@ -33,8 +33,6 @@ class SalesTaxesandCharges(Document):
dont_recompute_tax: DF.Check
included_in_paid_amount: 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
parent: DF.Data
parentfield: DF.Data

View File

@@ -35,7 +35,7 @@
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Rate",
"read_only": 1,
@@ -62,7 +62,7 @@
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Amount",
"read_only": 1,
@@ -91,16 +91,15 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-10 08:06:40.611761",
"modified": "2024-03-27 13:10:39.866399",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
amount: DF.Int
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Currency
rate: DF.Int
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -25,6 +25,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account_currency
class InvoiceCancelled(frappe.ValidationError):
@@ -410,10 +411,7 @@ class Subscription(Document):
invoice.customer = self.party
else:
invoice.supplier = self.party
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:
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
invoice.apply_tds = 1
# Add currency to invoice
@@ -431,6 +429,7 @@ class Subscription(Document):
items_list = self.get_items_from_plans(self.plans, is_prorate())
for item in items_list:
item["cost_center"] = self.cost_center
invoice.append("items", item)
# Taxes

View File

@@ -653,7 +653,7 @@ def reset_settings():
def create_subscription(**kwargs):
subscription = frappe.new_doc("Subscription")
subscription.party_type = kwargs.get("party_type") or "Customer"
subscription.party_type = (kwargs.get("party_type") or "Customer",)
subscription.company = kwargs.get("company") or "_Test Company"
subscription.party = kwargs.get("party") or "_Test Customer"
subscription.trial_period_start = kwargs.get("trial_period_start")

View File

@@ -0,0 +1,47 @@
{
"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": []
}

View File

@@ -1,11 +1,11 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TaxWithholdingGroup(Document):
class TaxWithheldVouchers(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -14,7 +14,12 @@ class TaxWithholdingGroup(Document):
if TYPE_CHECKING:
from frappe.types import DF
group_name: DF.Data
parent: 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
pass

View File

@@ -16,54 +16,4 @@ 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");
}

View File

@@ -10,12 +10,10 @@
"field_order": [
"category_details_section",
"category_name",
"tax_deduction_basis",
"column_break_2",
"round_off_tax_amount",
"column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"disable_cumulative_threshold",
"disable_transaction_threshold",
"section_break_8",
"rates",
"section_break_7",
@@ -63,7 +61,14 @@
},
{
"default": "0",
"description": "Tax withheld only for amount exceeding cumulative threshold",
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"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",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount "
@@ -74,28 +79,6 @@
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"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,

View File

@@ -1,15 +1,14 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import getdate
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt, getdate
from erpnext import allow_regional
from erpnext.controllers.accounts_controller import validate_account_head
@@ -29,41 +28,30 @@ class TaxWithholdingCategory(Document):
accounts: DF.Table[TaxWithholdingAccount]
category_name: DF.Data | None
disable_cumulative_threshold: DF.Check
disable_transaction_threshold: DF.Check
consider_party_ledger_amount: DF.Check
rates: DF.Table[TaxWithholdingRate]
round_off_tax_amount: DF.Check
tax_deduction_basis: DF.Literal["", "Gross Total", "Net Total"]
tax_on_excess_amount: DF.Check
# end: auto-generated types
def validate(self):
# TODO: Disable single threshold if tax on excess is enabled
self.validate_dates()
self.validate_companies_and_accounts()
self.validate_thresholds()
def validate_dates(self):
group_rates = defaultdict(list)
for d in self.get("rates"):
last_to_date = None
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
for d in rates:
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
group_rates[d.tax_withholding_group].append(d)
# Validate overlapping dates within each group
for group, rates in group_rates.items():
rates = sorted(rates, key=lambda d: getdate(d.from_date))
last_to_date = None
# validate overlapping of dates
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
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
last_to_date = d.to_date
def validate_companies_and_accounts(self):
existing_accounts = set()
@@ -90,168 +78,699 @@ class TaxWithholdingCategory(Document):
).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
frappe.throw(_("No Tax Withholding data found for the current posting date."))
def get_party_details(inv):
party_type, party = "", ""
def get_company_account(self, company):
for row in self.accounts:
if company == row.company:
return row.account
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
else:
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(
_("No Tax withholding account set for Company {0} in Tax Withholding Category {1}.").format(
frappe.bold(company), frappe.bold(self.name)
_(
"Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value."
).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:
d = frappe._dict(
{
"voucher_name": d.name,
"voucher_type": doctype,
"taxable_amount": d.base_net_total,
"grand_total": d.grand_total,
"posting_date": d.posting_date,
}
)
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
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
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.name = ja.parent
AND j.docstatus = 1
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:
vouchers.append(d.name)
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": d.name,
"voucher_type": "Journal Entry",
"taxable_amount": d.amount,
"reference_type": d.reference_type,
}
)
)
return vouchers, voucher_wise_amount
class TaxWithholdingDetails:
def __init__(
self,
tax_withholding_categories: list[str],
tax_withholding_group: str,
posting_date: str,
party_type: str,
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
def get(self) -> list:
"""
Fetches tax withholding categories based on the provided parameters.
"""
category_details = frappe._dict()
if not self.tax_withholding_categories:
return category_details
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
payment_entry_filters = {
"party_type": party_type,
"party": ("in", parties),
"docstatus": 1,
"apply_tax_withholding_amount": 1,
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"tax_withholding_category": tax_details.get("tax_withholding_category"),
"company": company,
}
ldc_details = self.get_ldc_details()
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
for category_name in self.tax_withholding_categories:
doc: TaxWithholdingCategory = frappe.get_cached_doc("Tax Withholding Category", category_name)
row = doc.get_applicable_tax_row(self.posting_date, self.tax_withholding_group)
account_head = doc.get_company_account(self.company)
category_detail = frappe._dict(
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,
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)
)
# ldc (only if valid based on posting date)
if ldc_detail := ldc_details.get(category_name):
category_detail.update(ldc_detail)
return tax_info
category_details[category_name] = category_detail
return category_details
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"
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 = {}
entries = frappe.db.get_all("GL Entry", filters, pluck=field)
return sum(entries)
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)
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
# 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)
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
tds_amount = 0
# map
for ldc in ldc_records:
category_name = ldc.tax_withholding_category
pi_grand_total = 0
pi_base_net_total = 0
jv_credit_amt = 0
pe_credit_amt = 0
unutilized_amount = ldc.certificate_limit - (ldc_utilization_map.get(ldc.name) or 0)
if not unutilized_amount:
continue
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)
ldc_details[category_name] = dict(
ldc_certificate=ldc.name,
ldc_unutilized_amount=unutilized_amount,
ldc_rate=ldc.rate,
)
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
jv_credit_amt += row.get("taxable_amount", 0)
return ldc_details
## 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,
}
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)
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,
}
)
)
query = query.where(ldc.pan_no == tax_id) if tax_id else query.where(ldc.supplier == self.party)
threshold = tax_details.get("threshold", 0)
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)
return query.run(as_dict=True)
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
def get_ldc_utilization_by_category(self, ldc_names, tax_id):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(twe.lower_deduction_certificate, Sum(twe.taxable_amount).as_("limit_consumed"))
.where(
(twe.company == self.company)
& (twe.party_type == self.party_type)
& (twe.tax_withholding_category.isin(self.tax_withholding_categories))
& (twe.lower_deduction_certificate.isin(ldc_names))
& (twe.docstatus == 1)
& (twe.status.isin(["Settled", "Over Withheld"]))
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
)
.groupby(twe.lower_deduction_certificate)
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):
tcs_amount = 0
ple = qb.DocType("Payment Ledger Entry")
# 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
)
query = query.where(twe.tax_id == tax_id) if tax_id else query.where(twe.party == self.party)
# sum of credit entries made from PE / JV with unset 'against voucher'
return frappe._dict(query.run())
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
@allow_regional
def get_tax_id_for_party(party_type, party):
return None
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

View File

@@ -1,237 +0,0 @@
{
"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": []
}

View File

@@ -1,20 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestTaxWithholdingEntry(IntegrationTestCase):
"""
Integration tests for TaxWithholdingEntry.
Use this class for testing interactions between multiple components.
"""
pass

View File

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

View File

@@ -1,48 +0,0 @@
{
"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": []
}

View File

@@ -1,20 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestTaxWithholdingGroup(IntegrationTestCase):
"""
Integration tests for TaxWithholdingGroup.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -7,11 +7,10 @@
"field_order": [
"from_date",
"to_date",
"tax_withholding_group",
"column_break_3",
"tax_withholding_rate",
"cumulative_threshold",
"single_threshold"
"column_break_3",
"single_threshold",
"cumulative_threshold"
],
"fields": [
{
@@ -31,14 +30,14 @@
"fieldname": "single_threshold",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Transaction Threshold"
"label": "Single Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Threshold"
"label": "Cumulative Transaction Threshold"
},
{
"columns": 2,
@@ -55,28 +54,20 @@
"in_list_view": 1,
"label": "To Date",
"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,
"istable": 1,
"links": [],
"modified": "2025-06-29 05:31:05.120377",
"modified": "2024-03-27 13:10:52.708165",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -20,7 +20,6 @@ class TaxWithholdingRate(Document):
parentfield: DF.Data
parenttype: DF.Data
single_threshold: DF.Float
tax_withholding_group: DF.Link | None
tax_withholding_rate: DF.Float
to_date: DF.Date
# end: auto-generated types

View File

@@ -210,20 +210,19 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
for d in gl_map:
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
if not from_repost:
validate_expense_against_budget(
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:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
@@ -427,11 +426,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.notify_update = False
gle.submit()
if (
not from_repost
and gle.voucher_type != "Period Closing Voucher"
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
):
if not from_repost and gle.voucher_type != "Period Closing Voucher":
validate_expense_against_budget(args)

View File

@@ -176,6 +176,10 @@ def _get_party_details(
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:
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
@@ -348,13 +352,10 @@ def set_contact_details(party_details, party, party_type):
def set_other_values(party_details, party, party_type):
# copy
to_copy = ["tax_withholding_category", "tax_withholding_group", "language"]
if party_type == "Customer":
to_copy.extend(["customer_name", "customer_group", "territory"])
to_copy = ["customer_name", "customer_group", "territory", "language"]
else:
to_copy.extend(["supplier_name", "supplier_group"])
to_copy = ["supplier_name", "supplier_group", "language"]
for f in to_copy:
party_details[f] = party.get(f)
@@ -1063,21 +1064,3 @@ def add_party_account(party_type, party, company, account):
def render_address(address, check_permissions=True):
return frappe.call(_render_address, address, check_permissions=check_permissions)
def validate_party_currency_before_merging(party_type, old_party, new_party):
for company in frappe.get_all("Company"):
old_party_currency = get_party_gle_currency(party_type, old_party, company.name)
new_party_currency = get_party_gle_currency(party_type, new_party, company.name)
if old_party_currency and new_party_currency and old_party_currency != new_party_currency:
frappe.throw(
_(
"Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'."
).format(
party_type,
old_party,
new_party,
company.name,
)
)

View File

@@ -1384,14 +1384,27 @@ class InitSQLProceduresForAR:
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
init_procedure_name = "ar_init_tmp_table"
init_procedure_sql = f"""
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
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)))
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
then
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);
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);
end if;
end;
"""
@@ -1433,13 +1446,16 @@ class InitSQLProceduresForAR:
end if;
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);
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);
end;
"""
def __init__(self):
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:
frappe.db.sql(self.init_procedure_sql)

View File

@@ -500,7 +500,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
immutable_ledger = frappe.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle, show_net_values=False):
def update_value_in_dict(data, key, gle):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -511,14 +511,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -552,11 +548,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -40,6 +41,16 @@ def _execute(filters=None, additional_table_columns=None):
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)
data = []
@@ -89,8 +100,8 @@ def _execute(filters=None, additional_table_columns=None):
for tax, details in itemised_tax.get(d.name, {}).items():
row.update(
{
f"{tax}_rate": details.get("tax_rate", 0),
f"{tax}_amount": details.get("tax_amount", 0),
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
}
)
if details.get("is_other_charges"):

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import functions as fn
from frappe.utils import flt
from frappe.utils import cstr, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
@@ -32,6 +32,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
return columns, [], None, None, None, 0
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))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -92,8 +101,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
for tax, details in itemised_tax.get(d.name, {}).items():
row.update(
{
f"{tax}_rate": details.get("tax_rate", 0),
f"{tax}_amount": details.get("tax_amount", 0),
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
}
)
if details.get("is_other_charges"):
@@ -558,24 +567,15 @@ def get_tax_accounts(
tax_details = query.run(as_dict=True)
precision = frappe.get_precision(tax_doctype, "tax_amount", currency=company_currency) or 2
tax_columns = {}
tax_columns = set()
itemised_tax = {}
scrubbed_description_map = {}
for row in tax_details:
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
tax_columns.add(description)
itemised_tax.setdefault(row.item_row, {}).setdefault(
scrubbed_description,
description,
frappe._dict(
{
"tax_rate": rate,
@@ -585,16 +585,14 @@ def get_tax_accounts(
),
)
itemised_tax[row.item_row][scrubbed_description].tax_amount += flt(row.amount, precision)
itemised_tax[row.item_row][description].tax_amount += flt(row.amount, precision)
tax_columns_list = list(tax_columns.keys())
tax_columns_list.sort()
for scrubbed_desc in tax_columns_list:
desc = tax_columns[scrubbed_desc]
tax_columns = sorted(tax_columns)
for desc in tax_columns:
columns.append(
{
"label": _(desc + " Rate"),
"fieldname": f"{scrubbed_desc}_rate",
"fieldname": frappe.scrub(desc + " Rate"),
"fieldtype": "Float",
"width": 100,
}
@@ -603,7 +601,7 @@ def get_tax_accounts(
columns.append(
{
"label": _(desc + " Amount"),
"fieldname": f"{scrubbed_desc}_amount",
"fieldname": frappe.scrub(desc + " Amount"),
"fieldtype": "Currency",
"options": "currency",
"width": 100,
@@ -641,7 +639,7 @@ def get_tax_accounts(
},
]
return itemised_tax, tax_columns_list
return itemised_tax, tax_columns
def get_tax_details_query(doctype, tax_doctype):
@@ -758,5 +756,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
total_row["percent_gt"] += item["percent_gt"]
for tax in tax_columns:
total_row.setdefault(f"{tax}_amount", 0.0)
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])

View File

@@ -14,8 +14,9 @@ frappe.query_reports["Tax Withholding Details"] = {
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Select",
options: ["", "Supplier", "Customer"],
default: "",
options: ["Supplier", "Customer"],
reqd: 1,
default: "Supplier",
on_change: function () {
frappe.query_report.set_filter_value("party", "");
},

View File

@@ -1,112 +1,194 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
"""Generate Tax Withholding Details report"""
validate_filters(filters)
if filters.get("party_type") == "Customer":
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)
(
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)
return columns, data
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
return columns, res
def validate_filters(filters):
"""Validate report filters"""
"""Validate if dates are properly set"""
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:
frappe.throw(_("From Date must be before To Date"))
def get_tax_withholding_data(filters):
"""Process entries into final report format"""
data = []
entries = get_tax_withholding_entries(filters)
if not entries:
return data
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map):
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
precision = get_currency_precision()
doc_info = get_additional_doc_info(entries)
party_details = get_party_details(entries)
out = []
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
for entry in entries:
doc_details = frappe._dict()
if entry.taxable_name:
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
if voucher_type == "Journal Entry":
party_list = journal_entry_party_map.get(name)
if party_list:
party = party_list[0]
party_info = party_details.get((entry.party_type, entry.party), {})
if entry.account in tds_accounts.keys():
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")
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)
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
# Sort by section code and transaction date
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
return data
values = net_total_map.get((voucher_type, name))
if values:
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_details(entries):
"""Fetch party details in batch for all entries"""
def get_party_pan_map(party_type):
party_map = frappe._dict()
parties_by_type = {"Customer": set(), "Supplier": set()}
# Group parties by type
for entry in entries:
if entry.party_type in parties_by_type and entry.party:
parties_by_type[entry.party_type].add(entry.party)
fields = ["name", "tax_withholding_category"]
if party_type == "Supplier":
fields += ["supplier_type", "supplier_name"]
else:
fields += ["customer_type", "customer_name"]
# Batch fetch for each party type
for party_type, party_set in parties_by_type.items():
if not party_type or not party_set:
continue
if frappe.db.has_column(party_type, "pan"):
fields.append("pan")
doctype = frappe.qb.DocType(party_type)
fields = [doctype.name]
party_details = frappe.db.get_all(party_type, fields=fields)
if party_type == "Supplier":
fields.extend([doctype.supplier_type.as_("entity_type"), doctype.supplier_name.as_("party_name")])
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
for party in party_details:
party.party_type = party_type
party_map[party.name] = party
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):
"""Generate report columns based on filters"""
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{
"label": _("Section Code"),
@@ -115,190 +197,286 @@ def get_columns(filters):
"fieldtype": "Link",
"width": 90,
},
{"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,
},
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
]
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
def get_tax_withholding_entries(filters):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(
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")
def get_tds_docs(filters):
tds_documents = []
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,
)
if filters.get("company"):
query = query.where(twe.company == filters.get("company"))
if filters.get("party_type"):
query = query.where(twe.party_type == filters.get("party_type"))
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 = (
frappe.qb.from_(gle)
.select("voucher_no", "voucher_type", "against", "party")
.where(gle.is_cancelled == 0)
)
if filters.get("from_date"):
query = query.where(gle.posting_date >= filters.get("from_date"))
if filters.get("to_date"):
query = query.where(gle.posting_date <= filters.get("to_date"))
if filters.get("party"):
query = query.where(twe.party == filters.get("party"))
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 == ""))
)
return query.run(as_dict=True)
query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
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_additional_doc_info(entries):
"""Fetch additional document information in batch"""
doc_info = {}
docs_by_type = {
"Purchase Invoice": set(),
"Sales Invoice": set(),
"Payment Entry": set(),
"Journal Entry": set(),
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{
"parent": ("in", journal_entries),
"party_type": ("in", ("Supplier", "Customer")),
"party": ("is", "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"],
}
# Group documents by type
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)
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
for entry in entries:
doc_info[(doctype_name, entry.name)] = entry
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
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

View File

@@ -2,18 +2,35 @@ import frappe
from frappe import _
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
get_tax_withholding_data,
get_result,
get_tds_docs,
)
from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
if filters.get("party_type") == "Customer":
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.update({"naming_series": party_naming_by})
validate_filters(filters)
data = get_tax_withholding_data(filters)
columns = get_columns(filters)
(
tds_docs,
tds_accounts,
tax_category_map,
journal_entry_party_map,
invoice_total_map,
) = get_tds_docs(filters)
final_result = group_by_party_and_category(data, filters)
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
)
final_result = group_by_party_and_category(res, filters)
return columns, final_result
@@ -38,6 +55,7 @@ def group_by_party_and_category(data, filters):
party_category_wise_map.setdefault(
(row.get("party"), row.get("section_code")),
{
"pan": row.get("pan"),
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_name": row.get("party_name"),
@@ -71,8 +89,9 @@ def get_final_result(party_category_wise_map):
def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
@@ -80,43 +99,47 @@ def get_columns(filters):
"options": "party_type",
"width": 180,
},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 120,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
]
if filters.naming_series == "Naming Series":
columns.append(
{
"label": _(filters.party_type + " Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
}
)
columns.extend(
[
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 120,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
]
)
return columns

View File

@@ -447,7 +447,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
}
for key in value_fields:
row[key] = flt(d.get(key, 0.0))
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values

View File

@@ -199,8 +199,6 @@ def get_balance_on(
ignore_account_permission=False,
account_type=None,
start_date=None,
finance_book=None,
include_default_fb_balances=False,
):
if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account")
@@ -295,31 +293,8 @@ def get_balance_on(
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
)
default_finance_book = None
if company:
cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
default_finance_book = frappe.get_cached_value("Company", company, "default_finance_book")
if finance_book:
if default_finance_book and include_default_fb_balances:
cond.append(
f"""(gle.finance_book IN (
{frappe.db.escape(finance_book)},
{frappe.db.escape(default_finance_book)}
) OR gle.finance_book IS NULL
)"""
)
else:
cond.append(f"(gle.finance_book = {frappe.db.escape(finance_book)} OR gle.finance_book IS NULL)")
elif default_finance_book and include_default_fb_balances:
# No finance book passed → fall back to default
cond.append(
f"""(
gle.finance_book = {frappe.db.escape(default_finance_book)}
OR gle.finance_book IS NULL
)"""
)
if account or (party_type and party) or account_type:
precision = get_currency_precision()
@@ -1358,7 +1333,7 @@ def get_children(doctype, parent, company, is_root=False, include_disabled=False
@frappe.whitelist()
def get_account_balances(accounts, company, finance_book=None, include_default_fb_balances=False):
def get_account_balances(accounts, company):
if isinstance(accounts, str):
accounts = loads(accounts)
@@ -1369,25 +1344,9 @@ def get_account_balances(accounts, company, finance_book=None, include_default_f
for account in accounts:
account["company_currency"] = company_currency
account["balance"] = flt(
get_balance_on(
account=account["value"],
in_account_currency=False,
company=company,
finance_book=finance_book,
include_default_fb_balances=include_default_fb_balances,
)
)
account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company))
if account["account_currency"] and account["account_currency"] != company_currency:
account["balance_in_account_currency"] = flt(
get_balance_on(
account=account["value"],
company=company,
finance_book=finance_book,
include_default_fb_balances=include_default_fb_balances,
)
)
account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
return accounts

View File

@@ -6,7 +6,7 @@
"label": "Profit and Loss"
}
],
"content": "[{\"id\":\"nDhfcJYbKH\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Payment\",\"col\":3}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Tax Masters\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}}]",
"content": "[{\"id\":\"nDhfcJYbKH\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Tax Masters\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0,
@@ -14,7 +14,7 @@
"for_user": "",
"hide_custom": 0,
"icon": "accounting",
"idx": 3,
"idx": 0,
"indicator_color": "",
"is_hidden": 0,
"label": "Accounting",
@@ -587,25 +587,25 @@
"type": "Link"
}
],
"modified": "2025-12-24 13:20:34.857205",
"modified": "2025-11-17 14:35:00.910131",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
"number_cards": [
{
"label": "Outgoing Bills",
"label": "Total Outgoing Bills",
"number_card_name": "Total Outgoing Bills"
},
{
"label": "Incoming Bills",
"label": "Total Incoming Bills",
"number_card_name": "Total Incoming Bills"
},
{
"label": "Incoming Payment",
"label": "Total Incoming Payment",
"number_card_name": "Total Incoming Payment"
},
{
"label": "Outgoing Payment",
"label": "Total Outgoing Payment",
"number_card_name": "Total Outgoing Payment"
}
],

View File

@@ -1,19 +1,13 @@
{
"app": "erpnext",
"charts": [
{
"chart_name": "Profit and Loss",
"label": "Profit and Loss"
}
],
"content": "[{\"id\":\"tS7ZWzC24I\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"8Ej2KxPxOt\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Ledgers\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"charts": [],
"content": "[{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Ledgers\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2024-01-05 16:09:16.766939",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "table",
"icon": "file",
"idx": 0,
"indicator_color": "",
"is_hidden": 0,
@@ -266,7 +260,7 @@
"type": "Link"
}
],
"modified": "2025-12-24 12:49:25.266357",
"modified": "2024-01-18 22:13:07.596844",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Reports",
@@ -279,6 +273,5 @@
"roles": [],
"sequence_id": 5.0,
"shortcuts": [],
"title": "Financial Reports",
"type": "Workspace"
}
"title": "Financial Reports"
}

View File

@@ -0,0 +1,204 @@
{
"charts": [],
"content": "[{\"id\":\"rMMsfn2eB4\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Shortcuts</b></span>\",\"col\":12}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Payable\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jAcOH-cC-Q\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"7dj93PEUjW\",\"type\":\"card\",\"data\":{\"card_name\":\"Invoicing\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"9yseIkdG50\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2024-01-05 15:29:11.144373",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "arrow-left",
"idx": 0,
"indicator_color": "",
"is_hidden": 0,
"label": "Payables",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Invoicing",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Invoice",
"link_count": 0,
"link_to": "Purchase Invoice",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_to": "Supplier",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
"link_count": 3,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Payment Entry",
"link_count": 0,
"link_to": "Payment Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Journal Entry",
"link_count": 0,
"link_to": "Journal Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_count": 0,
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Reports",
"link_count": 7,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Accounts Payable",
"link_count": 0,
"link_to": "Accounts Payable",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Accounts Payable Summary",
"link_count": 0,
"link_to": "Accounts Payable Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Register",
"link_count": 0,
"link_to": "Purchase Register",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Item-wise Purchase Register",
"link_count": 0,
"link_to": "Item-wise Purchase Register",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Order Analysis",
"link_count": 0,
"link_to": "Purchase Order Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Received Items To Be Billed",
"link_count": 0,
"link_to": "Received Items To Be Billed",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Supplier Ledger Summary",
"link_count": 0,
"link_to": "Supplier Ledger Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2024-01-18 22:09:46.221549",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payables",
"number_cards": [],
"owner": "Administrator",
"parent_page": "Accounting",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 3.0,
"shortcuts": [
{
"doc_view": "",
"label": "Accounts Payable",
"link_to": "Accounts Payable",
"type": "Report"
},
{
"doc_view": "",
"label": "Purchase Invoice",
"link_to": "Purchase Invoice",
"type": "DocType"
},
{
"doc_view": "",
"label": "Journal Entry",
"link_to": "Journal Entry",
"type": "DocType"
},
{
"doc_view": "",
"label": "Payment Entry",
"link_to": "Payment Entry",
"type": "DocType"
}
],
"title": "Payables"
}

View File

@@ -0,0 +1,254 @@
{
"charts": [],
"content": "[{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Shortcuts</b></span>\",\"col\":12}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"5yHldR0JNk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"POS Invoice\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"ILlIxJuexy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Cost Center\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"jLgv00c6ek\",\"type\":\"card\",\"data\":{\"card_name\":\"Invoicing\",\"col\":4}},{\"id\":\"npwfXlz0u1\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"am70C27Jrb\",\"type\":\"card\",\"data\":{\"card_name\":\"Dunning\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2024-01-05 15:29:21.084241",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "arrow-right",
"idx": 0,
"indicator_color": "",
"is_hidden": 0,
"label": "Receivables",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Invoicing",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Invoice",
"link_count": 0,
"link_to": "Sales Invoice",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customer",
"link_count": 0,
"link_to": "Customer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
"link_count": 4,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Entry",
"link_count": 0,
"link_to": "Payment Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Request",
"link_count": 0,
"link_to": "Payment Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_count": 0,
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Gateway Account",
"link_count": 0,
"link_to": "Payment Gateway Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dunning",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dunning",
"link_count": 0,
"link_to": "Dunning",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dunning Type",
"link_count": 0,
"link_to": "Dunning Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Reports",
"link_count": 6,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Accounts Receivable",
"link_count": 0,
"link_to": "Accounts Receivable",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Accounts Receivable Summary",
"link_count": 0,
"link_to": "Accounts Receivable Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Register",
"link_count": 0,
"link_to": "Sales Register",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Item-wise Sales Register",
"link_count": 0,
"link_to": "Item-wise Sales Register",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Order Analysis",
"link_count": 0,
"link_to": "Sales Order Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Delivered Items To Be Billed",
"link_count": 0,
"link_to": "Delivered Items To Be Billed",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2024-01-18 22:11:51.474477",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Receivables",
"number_cards": [],
"owner": "Administrator",
"parent_page": "Accounting",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 4.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "List",
"label": "POS Invoice",
"link_to": "POS Invoice",
"stats_filter": "[]",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Cost Center",
"link_to": "Cost Center",
"type": "DocType"
},
{
"doc_view": "",
"label": "Sales Invoice",
"link_to": "Sales Invoice",
"stats_filter": "[]",
"type": "DocType"
},
{
"doc_view": "",
"label": "Journal Entry",
"link_to": "Journal Entry",
"type": "DocType"
},
{
"doc_view": "",
"label": "Payment Entry",
"link_to": "Payment Entry",
"type": "DocType"
},
{
"doc_view": "",
"label": "Accounts Receivable",
"link_to": "Accounts Receivable",
"type": "Report"
}
],
"title": "Receivables"
}

View File

@@ -80,12 +80,6 @@ frappe.ui.form.on("Asset", {
}
},
before_submit: function (frm) {
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) {
frappe.throw(__("Please capitalize this asset before submitting."));
}
},
refresh: function (frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -202,10 +196,9 @@ frappe.ui.form.on("Asset", {
asset: frm.doc.name,
},
callback: function (r) {
frm.has_active_capitalization = r.message;
if (!r.message) {
$(".form-message").text(__("Capitalize this asset before submitting."));
$(".primary-action").prop("hidden", true);
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");
@@ -277,14 +270,8 @@ frappe.ui.form.on("Asset", {
const row = [
sch["idx"],
frappe.format(sch["schedule_date"], { fieldtype: "Date" }),
frappe.format(sch["depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
frappe.format(sch["accumulated_depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
frappe.format(sch["depreciation_amount"], { fieldtype: "Currency" }),
frappe.format(sch["accumulated_depreciation_amount"], { fieldtype: "Currency" }),
sch["journal_entry"] || "",
];
@@ -477,6 +464,7 @@ frappe.ui.form.on("Asset", {
is_composite_asset: function (frm) {
if (frm.doc.is_composite_asset) {
frm.set_value("net_purchase_amount", 0);
frm.set_df_property("net_purchase_amount", "read_only", 1);
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
}
@@ -544,6 +532,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
$(".primary-action").prop("hidden", false);
},
});
},

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