feat: Introduce tax withholding entry

(cherry picked from commit c66f78c784)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.py
#	erpnext/accounts/doctype/payment_entry/payment_entry.json
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
#	erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
#	erpnext/patches.txt
This commit is contained in:
Lakshit Jain
2025-12-23 20:47:53 +05:30
committed by Mergify
parent 904840c5b3
commit 283221c9ff
74 changed files with 7203 additions and 1899 deletions

View File

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

View File

@@ -1,27 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class AdvanceTax(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
account_head: DF.Link | None
allocated_amount: DF.Currency
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
reference_detail: DF.Data | None
reference_name: DF.DynamicLink | None
reference_type: DF.Link | None
# end: auto-generated types
pass

View File

@@ -14,6 +14,7 @@
"description",
"included_in_paid_amount",
"set_by_item_tax_template",
"is_tax_withholding_account",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -25,7 +26,6 @@
"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,12 +172,6 @@
"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",
@@ -213,18 +207,26 @@
"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": "2024-11-22 19:16:22.346267",
"modified": "2025-12-15 06:42:18.707671",
"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,7 +17,6 @@ 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
@@ -28,10 +27,12 @@ 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

@@ -201,6 +201,7 @@ 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) {
@@ -251,6 +252,10 @@ frappe.ui.form.on("Journal Entry", {
});
}
},
apply_tds: function (frm) {
frm.clear_table("tax_withholding_entries");
},
});
var update_jv_details = function (doc, r) {

View File

@@ -43,6 +43,11 @@
"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",
@@ -517,7 +522,7 @@
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
"label": "Consider for Tax Withholding "
},
{
"depends_on": "eval:doc.docstatus",
@@ -586,6 +591,39 @@
"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",
@@ -600,7 +638,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-09-29 13:05:46.982277",
"modified": "2025-11-13 17:54:14.542903",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -17,9 +17,7 @@ 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_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -49,6 +47,7 @@ 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,6 +64,7 @@ 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
@@ -73,6 +73,7 @@ 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
@@ -84,6 +85,8 @@ 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
@@ -150,8 +153,8 @@ class JournalEntry(AccountsController):
self.validate_company_in_accounting_dimension()
self.validate_advance_accounts()
if self.docstatus == 0:
self.apply_tax_withholding()
JournalTaxWithholding(self).on_validate()
if self.is_new() or not self.title:
self.title = self.get_title()
@@ -199,6 +202,7 @@ 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):
@@ -282,6 +286,8 @@ 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 = (
@@ -295,8 +301,10 @@ 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()
@@ -352,6 +360,7 @@ class JournalEntry(AccountsController):
StockAccountInvalidTransaction,
)
<<<<<<< HEAD
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
@@ -441,6 +450,8 @@ class JournalEntry(AccountsController):
for d in to_remove:
self.remove(d)
=======
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)
def update_asset_value(self):
self.update_asset_on_depreciation()
self.update_asset_on_disposal()

View File

@@ -41,6 +41,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.is_new()) {
set_default_party_type(frm);
frm.clear_table("tax_withholding_entries");
}
},
@@ -532,6 +533,7 @@ frappe.ui.form.on("Payment Entry", {
},
() => frm.set_value("party_name", r.message.party_name),
() => frm.clear_table("references"),
() => frm.clear_table("tax_withholding_entries"),
() => frm.events.hide_unhide_fields(frm),
() => frm.events.set_dynamic_labels(frm),
() => {
@@ -564,14 +566,15 @@ frappe.ui.form.on("Payment Entry", {
}
},
apply_tax_withholding_amount: function (frm) {
if (!frm.doc.apply_tax_withholding_amount) {
apply_tds: function (frm) {
if (!frm.doc.apply_tds) {
frm.set_value("tax_withholding_category", "");
} else {
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
frm.set_value("tax_withholding_category", values.tax_withholding_category);
});
}
frm.clear_table("tax_withholding_entries");
},
paid_from: function (frm) {

View File

@@ -21,6 +21,8 @@
"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",
@@ -60,10 +62,6 @@
"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",
@@ -71,6 +69,11 @@
"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",
@@ -578,14 +581,15 @@
"label": "Custom Remarks"
},
{
"depends_on": "eval:doc.apply_tax_withholding_amount",
"depends_on": "eval:doc.apply_tds",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"mandatory_depends_on": "eval:doc.apply_tds",
"options": "Tax Withholding Category"
},
{
<<<<<<< HEAD
"default": "0",
"depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "apply_tax_withholding_amount",
@@ -594,6 +598,8 @@
},
{
"collapsible": 1,
=======
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)
"fieldname": "taxes_and_charges_section",
"fieldtype": "Section Break",
"label": "Taxes and Charges"
@@ -648,15 +654,6 @@
"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",
@@ -753,6 +750,46 @@
"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,

View File

@@ -30,9 +30,7 @@ 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_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PaymentTaxWithholding
from erpnext.accounts.general_ledger import (
make_gl_entries,
make_reverse_gl_entries,
@@ -80,9 +78,10 @@ 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_tax_withholding_amount: DF.Check
apply_tds: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
@@ -103,11 +102,13 @@ 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
@@ -139,6 +140,8 @@ 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
@@ -189,7 +192,7 @@ class PaymentEntry(AccountsController):
self.validate_allocated_amount()
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
self.set_tax_withholding()
PaymentTaxWithholding(self).on_validate()
self.set_status()
self.set_total_in_words()
@@ -199,6 +202,7 @@ 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()
@@ -300,8 +304,10 @@ 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)
@@ -937,93 +943,6 @@ 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,14 +59,15 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-05 16:07:47.307971",
"modified": "2025-08-13 06:52:46.130142",
"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

@@ -223,7 +223,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
});
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
}
@@ -363,10 +362,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
},
function () {
me.apply_pricing_rule();
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);
me.frm.doc.apply_tds =
me.frm.tax_withholding_category || me.frm.tax_withholding_group ? 1 : 0;
me.frm.clear_table("tax_withholding_entries");
// while duplicating, don't change payment terms
if (me.frm.doc.__run_link_triggers === false) {
@@ -379,26 +377,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
apply_tds(frm) {
var me = this;
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");
me.frm.clear_table("tax_withholding_entries");
}
credit_to() {
@@ -702,10 +681,7 @@ 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.supplier_tds ? 1 : 0;
}
if (!frm.doc.__onload.supplier_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
}
}
@@ -714,7 +690,7 @@ frappe.ui.form.on("Purchase Invoice", {
});
if (frm.is_new()) {
frm.clear_table("tax_withheld_vouchers");
frm.clear_table("tax_withholding_entries");
}
},
@@ -741,6 +717,7 @@ 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,7 +27,6 @@
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"tax_withholding_category",
"amended_from",
"supplier_invoice_details",
"bill_no",
@@ -68,8 +67,6 @@
"column_break_28",
"total",
"net_total",
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
@@ -102,14 +99,17 @@
"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,7 +130,6 @@
"only_include_allocated_payments",
"get_advances",
"advances",
"advance_tax",
"write_off",
"write_off_amount",
"base_write_off_amount",
@@ -286,7 +285,7 @@
"default": "0",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount",
"label": "Consider for Tax Withholding",
"print_hide": 1
},
{
@@ -1358,14 +1357,6 @@
"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",
@@ -1455,14 +1446,6 @@
"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",
@@ -1477,42 +1460,6 @@
"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",
@@ -1662,7 +1609,7 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
},
{
"fieldname": "claimed_landed_cost_amount",
"fieldtype": "Currency",
@@ -1679,6 +1626,40 @@
"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,
@@ -1686,7 +1667,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 19:19:11.380664",
"modified": "2025-12-15 06:41:38.237728",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -24,9 +24,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_linked_doc,
validate_inter_company_party,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PurchaseTaxWithholding
from erpnext.accounts.general_ledger import (
get_round_off_account_and_cost_center,
make_gl_entries,
@@ -61,7 +59,6 @@ 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
@@ -72,14 +69,13 @@ class PurchaseInvoice(BuyingController):
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
PurchaseTaxesandCharges,
)
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
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
@@ -94,7 +90,6 @@ 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
@@ -128,6 +123,7 @@ 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
@@ -149,6 +145,7 @@ 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]
@@ -198,9 +195,8 @@ class PurchaseInvoice(BuyingController):
supplier_warehouse: DF.Link | None
tax_category: DF.Link | None
tax_id: DF.ReadOnly | None
tax_withheld_vouchers: DF.Table[TaxWithheldVouchers]
tax_withholding_category: DF.Link | None
tax_withholding_net_total: DF.Currency
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
tax_withholding_group: DF.Link | None
taxes: DF.Table[PurchaseTaxesandCharges]
taxes_and_charges: DF.Link | None
taxes_and_charges_added: DF.Currency
@@ -245,11 +241,14 @@ class PurchaseInvoice(BuyingController):
def onload(self):
super().onload()
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
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)
if self.is_new():
self.set("tax_withheld_vouchers", [])
self.set("tax_withholding_entries", [])
def before_save(self):
if not self.on_hold:
@@ -300,6 +299,7 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("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,11 +352,13 @@ class PurchaseInvoice(BuyingController):
template_name=self.payment_terms_template,
)
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)
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
super().set_missing_values(for_validate)
@@ -747,6 +749,7 @@ class PurchaseInvoice(BuyingController):
def on_submit(self):
super().on_submit()
PurchaseTaxWithholding(self).on_submit()
self.check_prev_docstatus()
@@ -788,7 +791,6 @@ 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()
@@ -1672,6 +1674,7 @@ 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()
@@ -1718,10 +1721,9 @@ 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()
@@ -1844,102 +1846,6 @@ 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"):

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_tax_withholding_amount = 1
payment_entry.apply_tds = 1
payment_entry.tax_withholding_category = tax_withholding_category
payment_entry.save()
payment_entry.submit()
@@ -1591,12 +1591,26 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
self.assertEqual(expected_gle[i][1], gle.amount)
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
tax_allocated = sum(
[
entry.withholding_amount
for entry in payment_entry.get("tax_withholding_entries", [])
if entry.taxable_name
]
)
self.assertEqual(tax_allocated, 3000)
purchase_invoice.cancel()
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
tax_allocated = sum(
[
entry.withholding_amount
for entry in payment_entry.get("tax_withholding_entries", [])
if entry.taxable_name
]
)
self.assertEqual(tax_allocated, 0)
def test_purchase_gl_with_tax_withholding_tax(self):
company = "_Test Company"
@@ -1631,7 +1645,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
do_not_submit=1,
)
pi.apply_tds = 1
pi.tax_withholding_category = tax_withholding_category
pi.save()
pi.submit()

View File

@@ -44,6 +44,7 @@
"rate",
"amount",
"item_tax_template",
"tax_withholding_category",
"col_break4",
"base_rate",
"base_amount",
@@ -893,7 +894,7 @@
"default": "1",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply TDS"
"label": "Consider for Tax Withholding"
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
@@ -979,6 +980,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,

View File

@@ -90,6 +90,7 @@ 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,7 +34,8 @@
"base_net_amount",
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount"
"base_tax_amount_after_discount_amount",
"dont_recompute_tax"
],
"fields": [
{
@@ -205,11 +206,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",
@@ -262,13 +263,22 @@
"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-07-24 15:08:44.433022",
"modified": "2025-11-24 18:22:56.886010",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -32,6 +32,7 @@ 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
@@ -39,6 +40,7 @@ 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

@@ -24,6 +24,7 @@ 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;
@@ -381,6 +382,9 @@ 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();
}
);
@@ -597,6 +601,10 @@ 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
@@ -817,6 +825,16 @@ 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,6 +28,7 @@
"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",
@@ -90,6 +91,11 @@
"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",
@@ -2247,6 +2253,46 @@
"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,9 +26,7 @@ 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_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
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 (
@@ -77,6 +75,7 @@ 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
@@ -90,6 +89,7 @@ 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,6 +135,7 @@ 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
@@ -162,6 +163,7 @@ 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
@@ -214,6 +216,8 @@ 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
@@ -282,6 +286,7 @@ class SalesInvoice(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
<<<<<<< HEAD
def before_print(self, settings=None):
from frappe.contacts.doctype.address.address import get_address_display_list
@@ -334,6 +339,15 @@ class SalesInvoice(SellingController):
},
user=frappe.session.user,
)
=======
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)
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)
def validate(self):
self.validate_auto_set_posting_time()
@@ -344,7 +358,7 @@ class SalesInvoice(SellingController):
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
self.set_tax_withholding()
SalesTaxWithholding(self).on_validate()
self.validate_proj_cust()
self.validate_pos_return()
@@ -466,38 +480,6 @@ 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()
@@ -519,6 +501,8 @@ 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()
@@ -658,6 +642,7 @@ 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()
@@ -699,6 +684,7 @@ class SalesInvoice(SellingController):
"Unreconcile Payment Entries",
"Payment Ledger Entry",
"Serial and Batch Bundle",
"Tax Withholding Entry",
)
self.delete_auto_created_batches()

View File

@@ -43,12 +43,14 @@
"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",
@@ -986,6 +988,21 @@
"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,6 +24,7 @@ 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
@@ -95,6 +96,7 @@ 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,6 +14,7 @@
"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",
@@ -202,7 +203,7 @@
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Dont Recompute tax",
"label": "Don't Recompute Tax",
"print_hide": 1,
"read_only": 1
},
@@ -241,6 +242,13 @@
"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,6 +33,8 @@ 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

@@ -411,7 +411,10 @@ class Subscription(Document):
invoice.customer = self.party
else:
invoice.supplier = self.party
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
"Supplier", self.party, ["tax_withholding_category", "tax_withholding_group"]
)
if tax_withholding_category or tax_withholding_group:
invoice.apply_tds = 1
# Add currency to invoice

View File

@@ -1,47 +0,0 @@
{
"actions": [],
"autoname": "hash",
"creation": "2022-09-13 16:18:59.404842",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_name",
"taxable_amount"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Type"
},
{
"fieldname": "voucher_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Voucher Name"
},
{
"fieldname": "taxable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Taxable Amount",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -16,4 +16,54 @@ frappe.ui.form.on("Tax Withholding Category", {
}
});
},
refresh: function (frm) {
update_rates_read_only_state(frm);
},
disable_cumulative_threshold: function (frm) {
toggle_threshold_settings(frm, "disable_cumulative_threshold");
if (frm.doc.disable_cumulative_threshold) {
reset_rates_column(frm, "cumulative_threshold");
}
update_rates_read_only_state(frm);
},
disable_transaction_threshold: function (frm) {
toggle_threshold_settings(frm, "disable_transaction_threshold");
if (frm.doc.disable_transaction_threshold) {
reset_rates_column(frm, "single_threshold");
}
update_rates_read_only_state(frm);
},
});
function toggle_threshold_settings(frm, field_name) {
if (frm.doc[field_name]) {
const other_field =
field_name === "disable_cumulative_threshold"
? "disable_transaction_threshold"
: "disable_cumulative_threshold";
frm.set_value(other_field, 0);
}
}
function update_rates_read_only_state(frm) {
frm.fields_dict["rates"].grid.update_docfield_property(
"cumulative_threshold",
"read_only",
frm.doc.disable_cumulative_threshold
);
frm.fields_dict["rates"].grid.update_docfield_property(
"single_threshold",
"read_only",
frm.doc.disable_transaction_threshold
);
}
function reset_rates_column(frm, field_name) {
$.each(frm.doc.rates || [], function (i, row) {
row[field_name] = 0;
});
frm.refresh_field("rates");
}

View File

@@ -10,10 +10,12 @@
"field_order": [
"category_details_section",
"category_name",
"round_off_tax_amount",
"tax_deduction_basis",
"column_break_2",
"consider_party_ledger_amount",
"round_off_tax_amount",
"tax_on_excess_amount",
"disable_cumulative_threshold",
"disable_transaction_threshold",
"section_break_8",
"rates",
"section_break_7",
@@ -61,14 +63,7 @@
},
{
"default": "0",
"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",
"description": "Tax withheld only for amount exceeding cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount "
@@ -79,6 +74,28 @@
"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,14 +1,15 @@
# 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 _, qb
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt, getdate
from frappe.query_builder.functions import Sum
from frappe.utils import getdate
from erpnext import allow_regional
from erpnext.controllers.accounts_controller import validate_account_head
@@ -28,30 +29,41 @@ class TaxWithholdingCategory(Document):
accounts: DF.Table[TaxWithholdingAccount]
category_name: DF.Data | None
consider_party_ledger_amount: DF.Check
disable_cumulative_threshold: DF.Check
disable_transaction_threshold: DF.Check
rates: DF.Table[TaxWithholdingRate]
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):
last_to_date = None
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
for d in rates:
group_rates = defaultdict(list)
for d in self.get("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 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))
# 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
last_to_date = d.to_date
for d in rates:
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
frappe.throw(
_("Row #{0}: Dates overlapping with other row in group {1}").format(
d.idx, group or "Default"
)
)
last_to_date = d.to_date
def validate_companies_and_accounts(self):
existing_accounts = set()
@@ -78,74 +90,32 @@ 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
def get_party_details(inv):
party_type, party = "", ""
frappe.throw(_("No Tax Withholding data found for the current posting date."))
<<<<<<< HEAD
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
else:
party_type = "Supplier"
party = inv.supplier
=======
def get_company_account(self, company):
for row in self.accounts:
if company == row.company:
return row.account
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)
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(
<<<<<<< HEAD
_(
"Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value."
).format(tax_withholding_category, inv.company, party)
@@ -404,244 +374,155 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
if doctype != "Sales Invoice":
filters.update(
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
=======
_("No Tax withholding account set for Company {0} in Tax Withholding Category {1}.").format(
frappe.bold(company), frappe.bold(self.name)
)
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)
)
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,
}
)
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
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(
def get(self) -> list:
"""
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,
)
Fetches tax withholding categories based on the provided parameters.
"""
category_details = frappe._dict()
if not self.tax_withholding_categories:
return category_details
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,
}
ldc_details = self.get_ldc_details()
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,
)
# ldc (only if valid based on posting date)
if ldc_detail := ldc_details.get(category_name):
category_detail.update(ldc_detail)
category_details[category_name] = category_detail
return category_details
def get_ldc_details(self):
"""
Fetches the Lower Deduction Certificate (LDC) details for the given party.
Assumes that only one LDC per category can be valid at a time.
"""
ldc_details = {}
if self.party_type != "Supplier":
return ldc_details
# NOTE: This can be a configurable option
# To check if filter by tax_id is needed
tax_id = get_tax_id_for_party(self.party_type, self.party)
# ldc details
ldc_records = self.get_valid_ldc_records(tax_id)
if not ldc_records:
return ldc_details
ldc_names = [ldc.name for ldc in ldc_records]
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id)
# map
for ldc in ldc_records:
category_name = ldc.tax_withholding_category
unutilized_amount = ldc.certificate_limit - (ldc_utilization_map.get(ldc.name) or 0)
if not unutilized_amount:
continue
ldc_details[category_name] = dict(
ldc_certificate=ldc.name,
ldc_unutilized_amount=unutilized_amount,
ldc_rate=ldc.rate,
)
return ldc_details
def get_valid_ldc_records(self, tax_id):
ldc = frappe.qb.DocType("Lower Deduction Certificate")
query = (
frappe.qb.from_(ldc)
.select(
ldc.name,
ldc.tax_withholding_category,
ldc.rate,
ldc.certificate_limit,
)
.where(
(ldc.valid_from <= self.posting_date)
& (ldc.valid_upto >= self.posting_date)
& (ldc.company == self.company)
& ldc.tax_withholding_category.isin(self.tax_withholding_categories)
)
)
return vouchers, voucher_wise_amount
query = query.where(ldc.pan_no == tax_id) if tax_id else query.where(ldc.supplier == self.party)
return query.run(as_dict=True)
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
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,
}
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
"""
Use Payment Ledger to fetch unallocated Advance Payments
"""
if party_type == "Supplier":
return []
ple = qb.DocType("Payment Ledger Entry")
conditions = []
conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0)
conditions.append(ple.party_type == party_type)
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
if company:
conditions.append(ple.company == company)
if from_date and to_date:
conditions.append(ple.posting_date[from_date:to_date])
advances = qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1)
if advances:
advances = [x[0] for x in advances]
return advances
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
tax_info = []
if inv.get("advances"):
advances = [d.reference_name for d in inv.get("advances")]
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
tax_info = (
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
.run(as_dict=True)
)
return tax_info
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
"is_cancelled": 0,
"credit": [">", 0],
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"account": tax_details.account_head,
"voucher_no": ["in", taxable_vouchers],
}
field = "credit"
entries = frappe.db.get_all("GL Entry", filters, pluck=field)
return sum(entries)
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
"""
Only applies for Taxes deducted on Advance Payments
"""
advance_tax_from_across_fiscal_year = sum(
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
)
return advance_tax_from_across_fiscal_year
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
tds_amount = 0
pi_grand_total = 0
pi_base_net_total = 0
jv_credit_amt = 0
pe_credit_amt = 0
for row in voucher_wise_amount:
if row.voucher_type == "Purchase Invoice":
pi_grand_total += row.get("grand_total", 0)
pi_base_net_total += row.get("taxable_amount", 0)
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
jv_credit_amt += row.get("taxable_amount", 0)
## for TDS to be deducted on advances
pe_filters = {
"party_type": "Supplier",
"party": ("in", parties),
"docstatus": 1,
"apply_tax_withholding_amount": 1,
"unallocated_amount": (">", 0),
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"tax_withholding_category": tax_details.get("tax_withholding_category"),
"company": inv.company,
}
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
if consider_party_ledger_amt:
pe_filters.pop("apply_tax_withholding_amount", None)
pe_filters.pop("tax_withholding_category", None)
# Get Amount via payment entry
payment_entries = frappe.db.get_all(
"Payment Entry",
filters=pe_filters,
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
)
for row in payment_entries:
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
pe_credit_amt += value
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": row.name,
"voucher_type": "Payment Entry",
"taxable_amount": value,
}
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"]))
)
.groupby(twe.lower_deduction_certificate)
)
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)
query = query.where(twe.tax_id == tax_id) if tax_id else query.where(twe.party == self.party)
# 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
cumulative_threshold_breached = (
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
)
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
supp_credit_amt += pi_base_net_total
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(
supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
return tds_amount
return frappe._dict(query.run())
<<<<<<< HEAD
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
ple = qb.DocType("Payment Ledger Entry")
@@ -774,3 +655,8 @@ def normal_round(number):
number = int(number) + decimal_part
return number
=======
@allow_regional
def get_tax_id_for_party(party_type, party):
return None
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)

View File

@@ -0,0 +1,237 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-06-20 04:55:28.583171",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"section_break_krko",
"company",
"party_type",
"party",
"tax_id",
"column_break_egzm",
"tax_withholding_category",
"tax_withholding_group",
"taxable_amount",
"tax_rate",
"withholding_amount",
"target_section",
"taxable_doctype",
"taxable_name",
"taxable_date",
"currency",
"conversion_rate",
"column_break_fqoe",
"under_withheld_reason",
"lower_deduction_certificate",
"source_section",
"withholding_doctype",
"withholding_name",
"withholding_date",
"column_break_dahw",
"section_break_ggna",
"status",
"column_break_jfjf",
"created_by_migration"
],
"fields": [
{
"fieldname": "section_break_krko",
"fieldtype": "Section Break"
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"label": "Party Type",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"label": "Party",
"options": "party_type",
"read_only": 1
},
{
"fieldname": "tax_id",
"fieldtype": "Data",
"label": "Tax ID",
"read_only": 1
},
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"read_only": 1
},
{
"fieldname": "column_break_egzm",
"fieldtype": "Column Break"
},
{
"columns": 1,
"fieldname": "tax_rate",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Tax Rate"
},
{
"columns": 1,
"fieldname": "taxable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Base Taxable Amount",
"options": "Company:company:default_currency"
},
{
"description": "Transaction from which tax is withheld",
"fieldname": "source_section",
"fieldtype": "Section Break",
"label": "Deducted From"
},
{
"fieldname": "column_break_dahw",
"fieldtype": "Column Break"
},
{
"description": "Transaction for which tax is withheld",
"fieldname": "target_section",
"fieldtype": "Section Break",
"label": "Applicable For"
},
{
"fieldname": "column_break_fqoe",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ggna",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_jfjf",
"fieldtype": "Column Break"
},
{
"fieldname": "lower_deduction_certificate",
"fieldtype": "Link",
"label": "Lower Deduction Certificate",
"options": "Lower Deduction Certificate",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nSettled\nUnder Withheld\nOver Withheld\nDuplicate\nCancelled",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"read_only": 1
},
{
"fieldname": "withholding_doctype",
"fieldtype": "Link",
"label": "Withholding Document Type",
"options": "DocType"
},
{
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"label": "Withholding Document Name",
"options": "withholding_doctype"
},
{
"fieldname": "taxable_doctype",
"fieldtype": "Link",
"label": "Taxable Document Type",
"options": "DocType"
},
{
"fieldname": "taxable_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Taxable Document Name",
"options": "taxable_doctype"
},
{
"fieldname": "taxable_date",
"fieldtype": "Date",
"label": "Taxable Date",
"read_only": 1
},
{
"fieldname": "withholding_date",
"fieldtype": "Date",
"label": "Withholding Date",
"read_only": 1
},
{
"fieldname": "under_withheld_reason",
"fieldtype": "Select",
"label": "Under Withheld Reason",
"options": "\nThreshold Exemption\nLower Deduction Certificate",
"read_only": 1
},
{
"columns": 1,
"fieldname": "withholding_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Base Tax Withheld",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"columns": 1,
"fieldname": "tax_withholding_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax Withholding Group",
"options": "Tax Withholding Group",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"default": "0",
"fieldname": "created_by_migration",
"fieldtype": "Check",
"hidden": 1,
"label": "Created By Migration",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-22 09:07:26.701207",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Entry",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:group_name",
"creation": "2025-06-29 05:24:51.819891",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"group_name"
],
"fields": [
{
"fieldname": "group_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group Name",
"reqd": 1,
"unique": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-06-29 05:25:50.243710",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Group",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

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

View File

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

View File

@@ -7,10 +7,11 @@
"field_order": [
"from_date",
"to_date",
"tax_withholding_rate",
"tax_withholding_group",
"column_break_3",
"single_threshold",
"cumulative_threshold"
"tax_withholding_rate",
"cumulative_threshold",
"single_threshold"
],
"fields": [
{
@@ -30,14 +31,14 @@
"fieldname": "single_threshold",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Single Transaction Threshold"
"label": "Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
"label": "Cumulative Threshold"
},
{
"columns": 2,
@@ -54,20 +55,28 @@
"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": "2024-03-27 13:10:52.708165",
"modified": "2025-06-29 05:31:05.120377",
"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,6 +20,7 @@ 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

@@ -176,10 +176,6 @@ 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")
@@ -352,10 +348,13 @@ 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 = ["customer_name", "customer_group", "territory", "language"]
to_copy.extend(["customer_name", "customer_group", "territory"])
else:
to_copy = ["supplier_name", "supplier_group", "language"]
to_copy.extend(["supplier_name", "supplier_group"])
for f in to_copy:
party_details[f] = party.get(f)

View File

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

View File

@@ -1,194 +1,112 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
from frappe.query_builder.functions import IfNull
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["naming_series"] = party_naming_by
"""Generate Tax Withholding Details report"""
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)
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
return columns, res
return columns, data
def validate_filters(filters):
"""Validate if dates are properly set"""
"""Validate report filters"""
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_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()
def get_tax_withholding_data(filters):
"""Process entries into final report format"""
data = []
entries = get_tax_withholding_entries(filters)
if not entries:
return data
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
doc_info = get_additional_doc_info(entries)
party_details = get_party_details(entries)
if voucher_type == "Journal Entry":
party_list = journal_entry_party_map.get(name)
if party_list:
party = party_list[0]
for entry in entries:
doc_details = frappe._dict()
if entry.taxable_name:
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
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")
party_info = party_details.get((entry.party_type, entry.party), {})
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
row = {
"section_code": entry.tax_withholding_category,
"entity_type": party_info.get("entity_type"),
"rate": entry.tax_rate,
"total_amount": entry.taxable_amount,
"grand_total": doc_details.get("grand_total", 0),
"base_total": doc_details.get("base_total", 0),
"tax_amount": entry.withholding_amount,
"transaction_date": entry.withholding_date,
"transaction_type": entry.taxable_doctype,
"ref_no": entry.taxable_name,
"taxable_date": entry.taxable_date,
"supplier_invoice_no": doc_details.get("bill_no"),
"supplier_invoice_date": doc_details.get("bill_date"),
"withholding_doctype": entry.withholding_doctype,
"withholding_name": entry.withholding_name,
"party_name": party_info.get("party_name"),
"tax_id": entry.tax_id,
"party": entry.party,
"party_type": entry.party_type,
}
data.append(row)
values = net_total_map.get((voucher_type, name))
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
# Sort by section code and transaction date
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
return data
def get_party_pan_map(party_type):
def get_party_details(entries):
"""Fetch party details in batch for all entries"""
party_map = frappe._dict()
parties_by_type = {"Customer": set(), "Supplier": set()}
fields = ["name", "tax_withholding_category"]
if party_type == "Supplier":
fields += ["supplier_type", "supplier_name"]
else:
fields += ["customer_type", "customer_name"]
# 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)
if frappe.db.has_column(party_type, "pan"):
fields.append("pan")
# 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
party_details = frappe.db.get_all(party_type, fields=fields)
doctype = frappe.qb.DocType(party_type)
fields = [doctype.name]
for party in party_details:
party.party_type = party_type
party_map[party.name] = party
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
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):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
"""Generate report columns based on filters"""
columns = [
{
"label": _("Section Code"),
@@ -197,286 +115,190 @@ def get_columns(filters):
"fieldtype": "Link",
"width": 90,
},
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _(filters.get("party_type", "Party")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 60,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Reference Date"),
"fieldname": "taxable_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Transaction Type"),
"fieldname": "transaction_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Withholding Document"),
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"options": "withholding_doctype",
"width": 150,
},
]
if filters.naming_series == "Naming Series":
columns.append(
{
"label": _(filters.party_type + " Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
}
)
else:
columns.append(
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
}
)
columns.extend(
[
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
]
)
if filters.party_type == "Supplier":
columns.extend(
[
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
]
)
columns.extend(
[
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 60,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 120,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
]
)
return columns
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,
)
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")
def get_tax_withholding_entries(filters):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(gle)
.select("voucher_no", "voucher_type", "against", "party")
.where(gle.is_cancelled == 0)
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")
)
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("company"):
query = query.where(twe.company == filters.get("company"))
if filters.get("party_type"):
query = query.where(twe.party_type == filters.get("party_type"))
if 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 == ""))
)
query = query.where(twe.party == filters.get("party"))
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
return query.run(as_dict=True)
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"],
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(),
}
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
# 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)
for entry in entries:
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
doc_info[(doctype_name, entry.name)] = entry

View File

@@ -2,35 +2,18 @@ import frappe
from frappe import _
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
get_result,
get_tds_docs,
get_tax_withholding_data,
)
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)
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)
final_result = group_by_party_and_category(data, filters)
return columns, final_result
@@ -55,7 +38,6 @@ 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"),
@@ -89,9 +71,8 @@ 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": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
@@ -99,47 +80,43 @@ 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

@@ -90,31 +90,6 @@ frappe.ui.form.on("Purchase Order", {
prevent_past_schedule_dates(frm);
},
supplier: function (frm) {
// Do not update if inter company reference is there as the details will already be updated
if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return;
if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return;
erpnext.utils.get_party_details(
frm,
"erpnext.accounts.party.get_party_details",
{
posting_date: frm.doc.transaction_date,
bill_date: frm.doc.bill_date,
party: frm.doc.supplier,
party_type: "Supplier",
account: frm.doc.credit_to,
price_list: frm.doc.buying_price_list,
fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template),
},
function () {
frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1);
frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1);
}
);
},
get_materials_from_supplier: function (frm) {
let po_details = [];
@@ -162,15 +137,6 @@ frappe.ui.form.on("Purchase Order", {
frm.set_value("transaction_date", frappe.datetime.get_today());
}
if (frm.doc.__onload && frm.doc.supplier) {
if (frm.is_new()) {
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);
}
}
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc);
});
@@ -181,14 +147,6 @@ frappe.ui.form.on("Purchase Order", {
}
},
apply_tds: function (frm) {
if (!frm.doc.apply_tds) {
frm.set_value("tax_withholding_category", "");
} else {
frm.set_value("tax_withholding_category", frm.supplier_tds);
}
},
get_subcontracting_boms_for_finished_goods: function (fg_item) {
return frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom.get_subcontracting_boms_for_finished_goods",

View File

@@ -22,8 +22,6 @@
"schedule_date",
"column_break1",
"company",
"apply_tds",
"tax_withholding_category",
"is_subcontracted",
"has_unit_price_items",
"supplier_warehouse",
@@ -57,8 +55,6 @@
"column_break_26",
"total",
"net_total",
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"section_break_48",
"pricing_rules",
"raw_material_details",
@@ -1134,19 +1130,6 @@
"options": "Company",
"read_only": 1
},
{
"default": "0",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount"
},
{
"depends_on": "eval: doc.apply_tds",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
@@ -1224,28 +1207,6 @@
"label": "Additional Info",
"oldfieldtype": "Section Break"
},
{
"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
},
{
"fieldname": "column_break_99",
"fieldtype": "Column Break"

View File

@@ -15,9 +15,6 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_linked_doc,
validate_inter_company_party,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
@@ -61,7 +58,6 @@ class PurchaseOrder(BuyingController):
advance_payment_status: DF.Literal["Not Initiated", "Initiated", "Partially Paid", "Fully Paid"]
amended_from: DF.Link | None
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
apply_tds: DF.Check
auto_repeat: DF.Link | None
base_discount_amount: DF.Currency
base_grand_total: DF.Currency
@@ -69,7 +65,6 @@ class PurchaseOrder(BuyingController):
base_net_total: 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
@@ -157,8 +152,6 @@ class PurchaseOrder(BuyingController):
supplier_name: DF.Data | None
supplier_warehouse: DF.Link | None
tax_category: DF.Link | None
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
@@ -191,8 +184,6 @@ class PurchaseOrder(BuyingController):
]
def onload(self):
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
self.set_onload("can_update_items", self.can_update_items())
def before_validate(self):
@@ -204,9 +195,6 @@ class PurchaseOrder(BuyingController):
self.set_status()
# apply tax withholding only if checked and applicable
self.set_tax_withholding()
self.validate_supplier()
self.validate_schedule_date()
validate_for_items(self)
@@ -284,36 +272,6 @@ class PurchaseOrder(BuyingController):
[["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]]
)
def set_tax_withholding(self):
if not self.apply_tds:
return
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
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)
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
def validate_supplier(self):
prevent_po = frappe.db.get_value("Supplier", self.supplier, "prevent_pos")
if prevent_po:
@@ -695,13 +653,6 @@ class PurchaseOrder(BuyingController):
if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
def set_missing_values(self, for_validate=False):
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.set_onload("supplier_tds", tds_category)
super().set_missing_values(for_validate)
@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
@@ -839,10 +790,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.flags.ignore_permissions = ignore_permissions
set_missing_values(source, target)
# set tax_withholding_category from Purchase Order
if source.apply_tds and source.tax_withholding_category and target.apply_tds:
target.tax_withholding_category = source.tax_withholding_category
# Get the advance paid Journal Entries in Purchase Invoice Advance
if target.get("allocate_advances_automatically"):
target.set_advances()

View File

@@ -55,7 +55,6 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"apply_tds",
"section_break_29",
"net_rate",
"net_amount",
@@ -899,12 +898,6 @@
"fieldname": "column_break_54",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply TDS"
},
{
"fieldname": "wip_composite_asset",
"fieldtype": "Link",

View File

@@ -18,7 +18,6 @@ class PurchaseOrderItem(Document):
actual_qty: DF.Float
against_blanket_order: DF.Check
amount: DF.Currency
apply_tds: DF.Check
base_amount: DF.Currency
base_net_amount: DF.Currency
base_net_rate: DF.Currency

View File

@@ -37,9 +37,10 @@
"dashboard_tab",
"tax_tab",
"tax_id",
"column_break_27",
"tax_category",
"column_break_27",
"tax_withholding_category",
"tax_withholding_group",
"contact_and_address_tab",
"address_contacts",
"address_html",
@@ -480,6 +481,12 @@
"fieldtype": "Table",
"label": "Customer Numbers",
"options": "Customer Number At Supplier"
},
{
"fieldname": "tax_withholding_group",
"fieldtype": "Link",
"label": "Tax Withholding Group",
"options": "Tax Withholding Group"
}
],
"grid_page_length": 50,
@@ -493,7 +500,7 @@
"link_fieldname": "party"
}
],
"modified": "2025-04-27 12:07:10.859758",
"modified": "2025-06-29 05:30:50.398653",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -73,6 +73,7 @@ class Supplier(TransactionBase):
tax_category: DF.Link | None
tax_id: DF.Data | None
tax_withholding_category: DF.Link | None
tax_withholding_group: DF.Link | None
warn_pos: DF.Check
warn_rfqs: DF.Check
website: DF.Data | None

View File

@@ -296,8 +296,6 @@ class AccountsController(TransactionBase):
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
self.set_tax_withholding()
with temporary_flag("company", self.company):
validate_regional(self)
@@ -1034,6 +1032,12 @@ class AccountsController(TransactionBase):
):
item.set("is_fixed_asset", ret.get("is_fixed_asset", 0))
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
"tax_withholding_category",
):
if not item.get("tax_withholding_category") and ret.get("tax_withholding_category"):
item.set("tax_withholding_category", ret.get("tax_withholding_category"))
# Double check for cost center
# Items add via promotional scheme may not have cost center set
if hasattr(item, "cost_center") and not item.get("cost_center"):

View File

@@ -393,12 +393,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
if source.tax_withholding_category:
doc.set_onload("supplier_tds", source.tax_withholding_category)
elif doctype == "Delivery Note":
# manual additions to the return should hit the return warehous, too
doc.set_warehouse = default_warehouse_for_sales_return
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
doc.tax_withholding_group = source.tax_withholding_group
doc.ignore_tax_withholding_threshold = source.ignore_tax_withholding_threshold
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount
@@ -455,6 +457,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty
target_doc.pricing_rules = None
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -525,6 +528,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
target_doc.tax_withholding_category = source_doc.tax_withholding_category
target_doc.apply_tds = source_doc.apply_tds
elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(
@@ -556,6 +561,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if doctype == "Sales Invoice":
target_doc.sales_invoice_item = source_doc.name
target_doc.tax_withholding_category = source_doc.tax_withholding_category
target_doc.apply_tds = source_doc.apply_tds
else:
target_doc.pos_invoice_item = source_doc.name

View File

@@ -29,11 +29,6 @@ class calculate_taxes_and_totals:
frappe.flags.round_off_applicable_accounts = []
frappe.flags.round_row_wise_tax = frappe.get_single_value("Accounts Settings", "round_row_wise_tax")
if doc.get("round_off_applicable_accounts_for_tax_withholding"):
frappe.flags.round_off_applicable_accounts.append(
doc.round_off_applicable_accounts_for_tax_withholding
)
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
@@ -77,24 +72,11 @@ class calculate_taxes_and_totals:
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
self.calculate_tax_withholding_net_total()
self.calculate_taxes()
self.adjust_grand_total_for_inclusive_tax()
self.calculate_totals()
self.calculate_total_net_weight()
def calculate_tax_withholding_net_total(self):
if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0
sum_base_net_amount = 0
for item in self._items:
if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount
self.doc.tax_withholding_net_total = sum_net_amount
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
if self.doc.get("is_return") and self.doc.get("return_against"):
return
@@ -577,16 +559,7 @@ class calculate_taxes_and_totals:
current_net_amount = item.net_amount
# distribute the tax amount proportionally to each item row
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
current_tax_amount = 0.0
else:
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
else:
current_tax_amount = (
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
)
current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
elif tax.charge_type == "On Net Total":
if tax.account_head in item_tax_map:

View File

@@ -647,6 +647,10 @@ global_search_doctypes = {
],
}
ignore_links_on_delete = [
"Tax Withholding Entry",
]
additional_timeline_content = {"*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"]}

View File

@@ -306,7 +306,6 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v13_0.drop_unused_sle_index_parts
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
erpnext.patches.v14_0.update_partial_tds_fields
erpnext.patches.v14_0.create_incoterms_and_migrate_shipment
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry
@@ -450,3 +449,10 @@ erpnext.patches.v16_0.set_valuation_method_on_companies
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
erpnext.patches.v16_0.migrate_budget_records_to_new_structure
erpnext.patches.v16_0.populate_budget_distribution_total
<<<<<<< HEAD
=======
erpnext.patches.v16_0.set_mr_picked_qty
erpnext.patches.v16_0.update_tax_withholding_field_in_payment_entry
erpnext.patches.v16_0.migrate_tax_withholding_data
>>>>>>> c66f78c784 (feat: Introduce tax withholding entry)

View File

@@ -1,33 +0,0 @@
import frappe
from frappe.utils import nowdate
from erpnext.accounts.utils import FiscalYearError, get_fiscal_year
def execute():
# Only do for current fiscal year, no need to repost for all years
for company in frappe.get_all("Company"):
try:
fiscal_year_details = get_fiscal_year(date=nowdate(), company=company.name, as_dict=True)
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
frappe.qb.update(purchase_invoice).set(
purchase_invoice.tax_withholding_net_total, purchase_invoice.net_total
).set(purchase_invoice.base_tax_withholding_net_total, purchase_invoice.base_net_total).where(
purchase_invoice.company == company.name
).where(purchase_invoice.apply_tds == 1).where(
purchase_invoice.posting_date >= fiscal_year_details.year_start_date
).where(purchase_invoice.docstatus == 1).run()
purchase_order = frappe.qb.DocType("Purchase Order")
frappe.qb.update(purchase_order).set(
purchase_order.tax_withholding_net_total, purchase_order.net_total
).set(purchase_order.base_tax_withholding_net_total, purchase_order.base_net_total).where(
purchase_order.company == company.name
).where(purchase_order.apply_tds == 1).where(
purchase_order.transaction_date >= fiscal_year_details.year_start_date
).where(purchase_order.docstatus == 1).run()
except FiscalYearError:
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import frappe
from frappe.query_builder import DocType
def execute():
if not frappe.db.has_column("Payment Entry", "apply_tax_withholding_amount"):
return
pe = DocType("Payment Entry")
(frappe.qb.update(pe).set(pe.apply_tds, pe.apply_tax_withholding_amount)).run()

View File

@@ -131,6 +131,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frm.cscript.calculate_taxes_and_totals();
});
// Tax Withholding Entries - Auto calculate withholding amount when taxable amount or tax rate changes
frappe.ui.form.on("Tax Withholding Entry", "taxable_amount", function (frm, cdt, cdn) {
me.calculate_withholding_amount(frm, cdt, cdn);
});
frappe.ui.form.on("Tax Withholding Entry", "tax_rate", function (frm, cdt, cdn) {
me.calculate_withholding_amount(frm, cdt, cdn);
});
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function (frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
@@ -583,6 +592,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
calculate_withholding_amount(frm, cdt, cdn) {
// Calculate withholding amount: taxable_amount * tax_rate / 100
let row = frappe.get_doc(cdt, cdn);
let withholding_amount = flt(
(row.taxable_amount * row.tax_rate) / 100,
precision("withholding_amount", row)
);
// Set the calculated withholding amount
frappe.model.set_value(cdt, cdn, "withholding_amount", withholding_amount);
}
send_sms() {
var sms_man = new erpnext.SMSManager(this.frm.doc);
}

View File

@@ -108,7 +108,8 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
args: args,
callback: function (r) {
if (r.message) {
frm.supplier_tds = r.message.supplier_tds;
frm.tax_withholding_category = r.message.tax_withholding_category;
frm.tax_withholding_group = r.message.tax_withholding_group;
frm.updating_party_details = true;
frappe.run_serially([
() => frm.set_value(r.message),

View File

@@ -62,9 +62,10 @@
"tax_tab",
"taxation_section",
"tax_id",
"column_break_21",
"tax_category",
"column_break_21",
"tax_withholding_category",
"tax_withholding_group",
"accounting_tab",
"credit_limit_section",
"payment_terms",
@@ -605,6 +606,12 @@
"fieldtype": "Table",
"label": "Supplier Numbers",
"options": "Supplier Number At Customer"
},
{
"fieldname": "tax_withholding_group",
"fieldtype": "Link",
"label": "Tax Withholding Group",
"options": "Tax Withholding Group"
}
],
"icon": "fa fa-user",

View File

@@ -89,6 +89,7 @@ class Customer(TransactionBase):
tax_category: DF.Link | None
tax_id: DF.Data | None
tax_withholding_category: DF.Link | None
tax_withholding_group: DF.Link | None
territory: DF.Link | None
website: DF.Data | None
# end: auto-generated types

View File

@@ -105,7 +105,13 @@
"customer_details",
"customer_items",
"item_tax_section_break",
"section_break_oilf",
"column_break_aytr",
"taxes",
"section_break_fxqz",
"purchase_tax_withholding_category",
"column_break_ltlb",
"sales_tax_withholding_category",
"quality_tab",
"inspection_required_before_purchase",
"quality_inspection_template",
@@ -910,6 +916,37 @@
{
"fieldname": "column_break_wugd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_oilf",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "column_break_aytr",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_fxqz",
"fieldtype": "Section Break"
},
{
"fieldname": "purchase_tax_withholding_category",
"fieldtype": "Link",
"label": "Purchase Tax Withholding Category",
"depends_on": "is_purchase_item",
"options": "Tax Withholding Category"
},
{
"fieldname": "column_break_ltlb",
"fieldtype": "Column Break"
},
{
"fieldname": "sales_tax_withholding_category",
"fieldtype": "Link",
"label": "Sales Tax Withholding Category",
"depends_on": "is_sales_item",
"options": "Tax Withholding Category"
}
],
"icon": "fa fa-tag",

View File

@@ -128,11 +128,13 @@ class Item(Document):
over_billing_allowance: DF.Float
over_delivery_receipt_allowance: DF.Float
production_capacity: DF.Int
purchase_tax_withholding_category: DF.Link | None
purchase_uom: DF.Link | None
quality_inspection_template: DF.Link | None
reorder_levels: DF.Table[ItemReorder]
retain_sample: DF.Check
safety_stock: DF.Float
sales_tax_withholding_category: DF.Link | None
sales_uom: DF.Link | None
sample_quantity: DF.Int
serial_no_series: DF.Data | None

View File

@@ -59,8 +59,6 @@
"column_break_27",
"total",
"net_total",
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_charges_section",
"tax_category",
"taxes_and_charges",
@@ -1256,24 +1254,6 @@
"options": "Subcontracting Receipt",
"search_index": 1
},
{
"fieldname": "tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Tax Withholding Net Total",
"no_copy": 1,
"options": "currency",
"read_only": 1
},
{
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Tax Withholding Net Total",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",

View File

@@ -55,7 +55,6 @@ class PurchaseReceipt(BuyingController):
base_net_total: 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
@@ -138,7 +137,6 @@ class PurchaseReceipt(BuyingController):
supplier_name: DF.Data | None
supplier_warehouse: DF.Link | None
tax_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

View File

@@ -59,7 +59,6 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"apply_tds",
"section_break_29",
"net_rate",
"net_amount",
@@ -1107,14 +1106,6 @@
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"default": "1",
"fieldname": "apply_tds",
"fieldtype": "Check",
"hidden": 1,
"label": "Apply TDS",
"read_only": 1
},
{
"default": "0",
"fieldname": "return_qty_from_rejected_warehouse",

View File

@@ -17,7 +17,6 @@ class PurchaseReceiptItem(Document):
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
amount_difference_with_purchase_invoice: DF.Currency
apply_tds: DF.Check
asset_category: DF.Link | None
asset_location: DF.Link | None
barcode: DF.Data | None

View File

@@ -106,6 +106,9 @@ def get_item_details(
get_party_item_code(ctx, item, out)
if ctx.doctype in ["Sales Invoice", "Purchase Invoice"]:
get_tax_withholding_category(ctx, item, out)
if ctx.doctype in ["Sales Order", "Quotation"]:
set_valuation_rate(out, ctx)
@@ -1309,6 +1312,32 @@ def get_party_item_code(ctx: ItemDetailsCtx, item_doc, out: ItemDetails):
out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None
def get_tax_withholding_category(ctx: ItemDetailsCtx, item_doc, out: ItemDetails):
"""
Get tax withholding category for the item based on the transaction type and party.
"""
tax_withholding_category = None
field = (
"sales_tax_withholding_category"
if ctx.transaction_type == "selling"
else "purchase_tax_withholding_category"
)
if item_doc.get(field):
tax_withholding_category = item_doc.get(field)
elif ctx.transaction_type == "buying" and ctx.supplier:
tax_withholding_category = frappe.get_cached_value(
"Supplier", ctx.supplier, "tax_withholding_category"
)
elif ctx.transaction_type == "selling" and ctx.customer:
tax_withholding_category = frappe.get_cached_value(
"Customer", ctx.customer, "tax_withholding_category"
)
out.tax_withholding_category = tax_withholding_category
from erpnext.deprecation_dumpster import get_pos_profile_item_details

View File

@@ -968,7 +968,6 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"reset_value": True,
"condition": lambda doc: not doc.is_tax_withholding_account,
},
},
postprocess=post_process,