Compare commits

...

74 Commits

Author SHA1 Message Date
Lakshit Jain
283221c9ff 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
2025-12-23 15:18:40 +00:00
Smit Vora
904840c5b3 Merge pull request #51278 from frappe/mergify/bp/version-16-beta/pr-51248
fix: don't consider phantom item in additional costs in stock entry (backport #51248)
2025-12-23 16:19:10 +05:30
Smit Vora
f6d3bbdf06 test: ensure enough stock
(cherry picked from commit a441b1c53a)
2025-12-23 09:34:34 +00:00
Smit Vora
4d8227a042 fix: conditional check for phantom item if field exists
(cherry picked from commit a5e5365ba9)
2025-12-23 09:34:33 +00:00
Smit Vora
ffd33c904c fix: don't consider phantom item in additional costs in stock entry
(cherry picked from commit 6e8a6582a0)
2025-12-23 09:34:33 +00:00
Diptanil Saha
4ccd823dcc Merge pull request #51253 from frappe/mergify/bp/version-16-beta/pr-50775
fix: item price not considering based on valid_upto (backport #50558)
2025-12-22 13:53:21 +05:30
Diptanil Saha
cd7e9e48de chore: resolve conflict
(cherry picked from commit 31142b2f47)
2025-12-22 07:38:16 +00:00
Sherin KR
797b6937f2 fix: item price not considering based on valid_upto
(cherry picked from commit dfda8e6241)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py
(cherry picked from commit c5d92d7999)
2025-12-22 07:38:16 +00:00
ruthra kumar
0507dae04a Merge pull request #51107 from frappe/mergify/bp/version-16-beta/pr-50837
feat(accounting-period): add role-based bypass for accounting period restrictions (backport #50837)
2025-12-15 20:52:11 +05:30
Jatin3128
fb96bbb8f8 feat(accounting-period): add role-based bypass for accounting period restrictions
(cherry picked from commit cfdbeb6a1a)
2025-12-15 12:07:08 +00:00
Smit Vora
a18ef85283 Merge pull request #51104 from frappe/mergify/bp/version-16-beta/pr-50782
fix: only show net balance as opening in general ledger (backport #50782)
2025-12-15 15:50:33 +05:30
Smit Vora
4d8ba5cca4 fix: only show net gl balance as opening in general ledger
(cherry picked from commit b7c7e0746e)
2025-12-15 10:00:47 +00:00
Sagar Vora
e930702faa Merge pull request #51060 from frappe/mergify/bp/version-16-beta/pr-51057
fix: re-calculate outstanding / write-off amount during submission (backport #51057)
2025-12-11 23:26:22 +05:30
Sagar Vora
16fe099317 fix: re-calculate outstanding / write-off amount during submission
(cherry picked from commit 09c9ac1b66)
2025-12-11 17:35:30 +00:00
Sagar Vora
626eb0d28e Merge pull request #51054 from frappe/mergify/bp/version-16-beta/pr-51051
fix: ensure fresh `grand_total_diff` is used for each calculation (backport #51051)
2025-12-11 18:07:57 +05:30
Sagar Vora
857574be69 fix: ensure fresh grand_total_diff is used for each calculation
(cherry picked from commit b3fdef8d19)
2025-12-11 12:35:29 +00:00
Khushi Rawat
f9ff06d926 Merge pull request #51008 from frappe/mergify/bp/version-16-beta/pr-50804
fix: correct logic for repair cost in asset repair (backport #50804)
2025-12-10 14:03:34 +05:30
ljain112
af9f4c7dd1 chore: remove unused import for depreciation schedule
(cherry picked from commit e1fd90f731)
2025-12-10 07:28:53 +00:00
ljain112
b81e1f8fe9 chore: remove unwanted strings
(cherry picked from commit 8ee2cbf259)
2025-12-10 07:28:53 +00:00
ljain112
5279bec1b2 refactor: linters
(cherry picked from commit 2a0ba84f69)
2025-12-10 07:28:52 +00:00
ljain112
0257fc42bb fix: add permission check
(cherry picked from commit 0c1df30771)
2025-12-10 07:28:52 +00:00
ljain112
fef76907c3 fix: add duplicate purchase invoice validation in asset repair
(cherry picked from commit ff9b392024)
2025-12-10 07:28:52 +00:00
ljain112
f12319b123 perf: enhance validation for purchase invoices to check submission status for all invoices
(cherry picked from commit 0b84d11600)
2025-12-10 07:28:52 +00:00
ljain112
945568183c perf: replace get_doc with get_lazy_doc for asset retrieval and optimize stock entry fetching
(cherry picked from commit c2810ea799)
2025-12-10 07:28:51 +00:00
ljain112
8fa785a45b fix: update repair cost logic to set value only for positive amounts
(cherry picked from commit 00ffdee928)
2025-12-10 07:28:51 +00:00
ljain112
e50ba37108 fix: remove unnecessary filtering by search text in get_expense_accounts
(cherry picked from commit b9aaae6343)
2025-12-10 07:28:51 +00:00
ljain112
c06e121cda fix: correct logic for repair cost in asset repair
(cherry picked from commit e6160d1b63)
2025-12-10 07:28:51 +00:00
Khushi Rawat
b4da06459d Merge pull request #51006 from frappe/mergify/bp/version-16-beta/pr-50999
fix: better manual budget distribution (backport #50999)
2025-12-10 12:20:29 +05:30
Khushi Rawat
b84531d4ea fix: conflicts 2025-12-10 11:59:14 +05:30
khushi8112
07c83246d0 fix: patch to set budget distribution total
(cherry picked from commit ed4c17d3a2)

# Conflicts:
#	erpnext/patches.txt
2025-12-10 06:27:36 +00:00
khushi8112
e1d46d4dad feat: show budget distribution total
(cherry picked from commit f194ac093c)
2025-12-10 06:27:35 +00:00
khushi8112
341ea89d16 fix: better manual budget distribution on update
(cherry picked from commit 1c82f42fa8)
2025-12-10 06:27:35 +00:00
khushi8112
88af49f6c0 fix: remove revise budget permission
(cherry picked from commit d42aad18a7)
2025-12-10 06:27:35 +00:00
khushi8112
7ea82ce2f0 fix: add company-based filter to account field
(cherry picked from commit 6a03fc6ede)
2025-12-10 06:27:35 +00:00
khushi8112
77f70b4c26 fix: make amount and percent field read only when distribute equally is enabled
(cherry picked from commit 75999a7ae4)
2025-12-10 06:27:34 +00:00
Smit Vora
a0c768d55d Merge pull request #50866 from frappe/mergify/bp/version-16-beta/pr-50733 2025-12-02 15:15:36 +05:30
Smit Vora
79181e307c fix: use ValueWrapper consistently
(cherry picked from commit a2fadd9347)
2025-12-02 09:13:52 +00:00
Smit Vora
9f07579a05 refactor: further changes to adapt to query builder changes
(cherry picked from commit 8235a551f0)
2025-12-02 09:13:52 +00:00
Khushi Rawat
7bfdd4b0a8 Merge pull request #50861 from frappe/mergify/bp/version-16-beta/pr-50794
fix: use asset in against_voucher while posting gl entries for capitalised asset repairs (backport #50794)
2025-12-02 12:46:09 +05:30
Khushi Rawat
a2242439c8 Merge pull request #50859 from frappe/mergify/bp/version-16-beta/pr-50793
fix: include accounting dimensions in stock entries created during asset repair. (backport #50793)
2025-12-02 12:23:37 +05:30
Navin-S-R
8440a5c68d chore: reload asset doc before assertEqual
(cherry picked from commit 8c35a6ecdd)
2025-12-02 06:37:52 +00:00
Navin-S-R
665a092cd0 test: add unit test to validate capitalized asset repair gl entries being booked against the asset
(cherry picked from commit bcf6deec9a)
2025-12-02 06:37:52 +00:00
Navin S R
a6b7985eb2 fix: use asset in against_voucher while posting gl entries for capitalized asset repairs
(cherry picked from commit a7e43eddad)
2025-12-02 06:37:52 +00:00
ljain112
30f368c4c0 refactor: show_general ledger for consistency with other doctyoes
(cherry picked from commit cdbe8b909b)
2025-12-02 06:27:36 +00:00
ljain112
be37d44d4c fix: include accounting dimensions in stock entries created during asset repair.
(cherry picked from commit 147a5ee953)
2025-12-02 06:27:36 +00:00
ruthra kumar
7ecf86af18 Merge pull request #50806 from frappe/mergify/bp/version-16-beta/pr-50802
fix(accounts-payable-summary): add Show GL Balance check similar to A… (backport #50802)
2025-11-29 12:06:33 +05:30
Jatin3128
895694f870 fix(accounts-payable-summary): add Show GL Balance check similar to Accounts Receivable Summary
(cherry picked from commit 8a7e5d0626)
2025-11-29 06:04:29 +00:00
Khushi Rawat
370ddb181b Merge pull request #50801 from frappe/mergify/bp/version-16-beta/pr-50792
fix: add Stock Entry link to Asset Repair doctype. (backport #50792)
2025-11-29 00:27:53 +05:30
ljain112
db446f33ad fix: add Stock Entry link to Asset Repair doctype.
(cherry picked from commit da7f28a3c3)
2025-11-28 12:05:40 +00:00
mergify[bot]
b7d831cc47 fix: incorrect positional param for get_field_precision util (backport #50764) (#50796) 2025-11-28 14:18:33 +05:30
Smit Vora
b8e5f3f996 fix: restore missing account number for Indirect Expenses in standard COA with Numbers (backport #50767) (#50784)
Co-authored-by: Aadhil <36843795+aadhilpm@users.noreply.github.com>
fix: restore missing account number for Indirect Expenses in standard COA with Numbers (#50767)
2025-11-27 19:41:58 +05:30
Aadhil
b8ab6403ab fix: restore missing account number for Indirect Expenses in standard COA with Numbers (#50767)
(cherry picked from commit 9145bf5563)
2025-11-27 13:54:25 +00:00
Smit Vora
0ade33e46a feat: add Account Category field to Account (Chart of Accounts) (backport #50766) (#50783)
Co-authored-by: Aadhil <36843795+aadhilpm@users.noreply.github.com>
2025-11-27 19:19:09 +05:30
Aadhil
6b877af91d feat: add Account Category field to Account (Chart of Accounts) (#50766)
(cherry picked from commit 355aa52cb8)
2025-11-27 13:11:14 +00:00
Rohit Waghchaure
4bf4df0f1d Merge branch 'develop' into version-16-beta 2025-11-21 22:08:39 +05:30
Lakshit Jain
1b32889eed Merge pull request #50682 from frappe/mergify/bp/version-16-beta/pr-50609
fix: handle empty item_tax_rate in ItemTax class (backport #50609)
2025-11-21 18:46:38 +05:30
Karm Soni
538b54a2dc fix: handle empty item_tax_rate in ItemTax class
(cherry picked from commit fc098a732b)
2025-11-21 12:57:48 +00:00
Lakshit Jain
11eda0e051 Merge pull request #50676 from frappe/mergify/bp/version-16-beta/pr-50675
fix: ignore chunk if no valid invoices found (backport #50675)
2025-11-21 18:27:34 +05:30
Smit Vora
cca315d072 Merge pull request #50679 from frappe/mergify/bp/version-16-beta/pr-50658
fix: handle zero rate actual taxes in calculate_taxes_and_totals (backport #50658)
2025-11-21 17:10:21 +05:30
ljain112
843a38acfd fix: handle zero rate actual taxes in calculate_taxes_and_totals
(cherry picked from commit ef37e6aa16)
2025-11-21 11:14:41 +00:00
Sagar Vora
434d05f9cf fix: ignore chunk if no valid invoices found
(cherry picked from commit f644c19760)
2025-11-21 08:53:52 +00:00
ruthra kumar
71611f81c9 Merge pull request #50630 from frappe/mergify/bp/version-16-beta/pr-50623
fix: replace `this` with function path (backport #50623)
2025-11-19 16:21:09 +05:30
Smit Vora
635047c290 fix: replace this with function path
(cherry picked from commit 2a1eb08b08)
2025-11-19 10:49:36 +00:00
Mihir Kandoi
82ba11e432 Merge pull request #50616 from frappe/mergify/bp/version-16-beta/pr-50614
fix: redundant message on bom save (backport #50614)
2025-11-19 12:55:56 +05:30
Mihir Kandoi
9374958473 fix: redundant message on bom save
(cherry picked from commit 074f07694f)
2025-11-19 07:08:17 +00:00
ruthra kumar
fe73f43d8b Merge pull request #50604 from frappe/mergify/bp/version-16-beta/pr-50524
fix: use dynamic account type to get average ratio balance (backport #50524)
2025-11-18 17:44:25 +05:30
ruthra kumar
dd93af80e0 Merge pull request #50602 from frappe/mergify/bp/version-16-beta/pr-50516
fix(general_ledger): add translation for accounting dimension (backport #50516)
2025-11-18 17:13:19 +05:30
Navin-S-R
a2fe72013f fix: correct profit after tax calculation by reducing expenses from income
(cherry picked from commit f420371a7e)
2025-11-18 11:22:47 +00:00
Navin-S-R
2b1f31bacb fix: use dynamic account type to get average ratio balance
(cherry picked from commit 9118f08e7b)
2025-11-18 11:22:47 +00:00
Logesh Periyasamy
e87e9b4769 fix(general_ledger): add translation for accounting dimension
(cherry picked from commit 113ff17c71)
2025-11-18 11:16:36 +00:00
ruthra kumar
3830dc9099 Merge pull request #50586 from frappe/mergify/bp/version-16-beta/pr-50529
fix: unintended backported depends_on expression (backport #50529)
2025-11-18 15:05:49 +05:30
rohitwaghchaure
42c1f95981 Merge pull request #50590 from frappe/mergify/bp/version-16-beta/pr-50585
fix: icons for workspace sidebar in ERPNext modules (backport #50585)
2025-11-18 14:35:56 +05:30
Rohit Waghchaure
052ce11d0f fix: icons for workspace sidebar in ERPNext modules
(cherry picked from commit 842546d917)
2025-11-18 08:42:07 +00:00
Kavin
9cafee3b59 fix: unintended backported depends_on expression (#50529)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
(cherry picked from commit 81a16286a1)
2025-11-18 08:08:01 +00:00
99 changed files with 7991 additions and 2070 deletions

View File

@@ -160,6 +160,14 @@ frappe.treeview_settings["Account"] = {
.options,
description: __("Optional. This setting will be used to filter in various transactions."),
},
{
fieldtype: "Link",
fieldname: "account_category",
label: __("Account Category"),
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
.options,
description: __("Optional. Used with Financial Report Template"),
},
{
fieldtype: "Float",
fieldname: "tax_rate",

View File

@@ -228,6 +228,7 @@ def get():
},
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
"account_number": "5200",
},
"root_type": "Expense",
"account_number": "5000",

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:

View File

@@ -442,7 +442,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

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

@@ -301,7 +301,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(

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

@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{
fieldname: "show_gl_balance",
label: __("Show GL Balance"),
fieldtype: "Check",
},
],
onload: function (report) {

View File

@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
for party, party_dict in self.party_total.items():
if flt(party_dict.outstanding, self.currency_precision) == 0:
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
def get_gl_balance(report_date, company):
def get_gl_balance(report_date, company, account_type):
if account_type == "Payable":
balance_calc_fields = ["party", {"SUM": [{"SUB": ["credit", "debit"]}], "as": "balance"}]
else:
balance_calc_fields = ["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}]
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}],
fields=balance_calc_fields,
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,

View File

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

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

@@ -73,14 +73,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -124,24 +117,71 @@ frappe.ui.form.on("Asset Repair", {
frm.refresh_field("stock_items");
}
},
});
purchase_invoice: function (frm) {
if (frm.doc.purchase_invoice) {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Purchase Invoice",
fieldname: "base_net_total",
filters: { name: frm.doc.purchase_invoice },
},
callback: function (r) {
if (r.message) {
frm.set_value("repair_cost", r.message.base_net_total);
frappe.ui.form.on("Asset Repair Purchase Invoice", {
purchase_invoice: function (frm, cdt, cdn) {
frappe.model.set_value(cdt, cdn, {
expense_account: "",
repair_cost: 0,
});
},
expense_account: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (!row.purchase_invoice || !row.expense_account) {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
return;
}
frappe.call({
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_unallocated_repair_cost",
args: {
purchase_invoice: row.purchase_invoice,
expense_account: row.expense_account,
},
callback: function (r) {
if (r.message !== undefined) {
if (r.message > 0) {
frappe.model.set_value(cdt, cdn, "repair_cost", r.message);
} else {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
let pi_link = frappe.utils.get_form_link(
"Purchase Invoice",
row.purchase_invoice,
true
);
frappe.msgprint({
message: __(
"Row {0}: The entire expense amount for account {1} in {2} has already been allocated.",
[row.idx, row.expense_account.bold(), pi_link]
),
indicator: "orange",
});
}
}
},
});
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
});
} else {
frm.set_value("repair_cost", 0);
__("View")
);
}
},
});

View File

@@ -260,8 +260,13 @@
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-04 23:06:43.644846",
"links": [
{
"link_doctype": "Stock Entry",
"link_fieldname": "asset_repair"
}
],
"modified": "2025-11-28 13:04:34.921098",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -4,14 +4,17 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
reschedule_depreciation,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -56,7 +59,7 @@ class AssetRepair(AccountsController):
# end: auto-generated types
def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.asset_doc = frappe.get_lazy_doc("Asset", self.asset)
self.validate_asset()
self.validate_dates()
self.validate_purchase_invoices()
@@ -81,60 +84,91 @@ class AssetRepair(AccountsController):
)
def validate_purchase_invoices(self):
self.validate_duplicate_purchase_invoices()
self.validate_purchase_invoice_status()
for d in self.invoices:
self.validate_purchase_invoice_status(d.purchase_invoice)
invoice_items = self.get_invoice_items(d.purchase_invoice)
self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
self.validate_expense_account(d, invoice_items)
self.validate_purchase_invoice_repair_cost(d, invoice_items)
self.validate_expense_account(d)
self.validate_purchase_invoice_repair_cost(d)
def validate_purchase_invoice_status(self, purchase_invoice):
docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
)
def validate_duplicate_purchase_invoices(self):
# account wise duplicate check
purchase_invoices = set()
duplicates = []
for row in self.invoices:
key = (row.purchase_invoice, row.expense_account)
if key in purchase_invoices:
duplicates.append((row.idx, row.purchase_invoice, row.expense_account))
else:
purchase_invoices.add(key)
if duplicates:
duplicate_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)} - {frappe.bold(account)}</li>"
for idx, pi, account in duplicates
]
)
msg = _("The following rows are duplicates:") + f"<br><ul>{duplicate_links}</ul>"
frappe.throw(msg)
def get_invoice_items(self, pi):
invoice_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": pi},
fields=["item_code", "expense_account", "base_net_amount"],
def validate_purchase_invoice_status(self):
pi_names = [row.purchase_invoice for row in self.invoices]
docstatus = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ["in", pi_names]},
fields=["name", "docstatus"],
as_list=True,
)
)
return invoice_items
invalid_invoice = []
for row in self.invoices:
if docstatus.get(row.purchase_invoice) != 1:
invalid_invoice.append((row.idx, row.purchase_invoice))
def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
service_item_exists = False
for item in invoice_items:
if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
service_item_exists = True
break
if invalid_invoice:
invoice_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)}</li>"
for idx, pi in invalid_invoice
]
)
msg = _("The following Purchase Invoices are not submitted:") + f"<br><ul>{invoice_links}</ul>"
frappe.throw(msg)
if not service_item_exists:
def validate_expense_account(self, row):
"""Validate that the expense account exists in the purchase invoice for non-stock items."""
valid_accounts = _get_expense_accounts_for_purchase_invoice(row.purchase_invoice)
if row.expense_account not in valid_accounts:
frappe.throw(
_("Service item not present in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
_(
"Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. "
"Only expense accounts from non-stock items are allowed."
).format(
row.idx,
frappe.bold(row.expense_account),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
)
)
def validate_expense_account(self, row, invoice_items):
pi_expense_accounts = set([item.expense_account for item in invoice_items])
if row.expense_account not in pi_expense_accounts:
frappe.throw(
_("Expense account {0} not present in Purchase Invoice {1}").format(
row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice)
)
)
def validate_purchase_invoice_repair_cost(self, row):
"""Validate that repair cost doesn't exceed available amount."""
available_amount = get_unallocated_repair_cost(
row.purchase_invoice, row.expense_account, exclude_asset_repair=self.name
)
def validate_purchase_invoice_repair_cost(self, row, invoice_items):
pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items])
if flt(row.repair_cost) > pi_net_total:
if flt(row.repair_cost) > available_amount:
frappe.throw(
_("Repair cost cannot be greater than purchase invoice base net total {0}").format(
pi_net_total
_(
"Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}"
).format(
row.idx,
frappe.bold(frappe.format_value(row.repair_cost, {"fieldtype": "Currency"})),
frappe.bold(frappe.format_value(available_amount, {"fieldtype": "Currency"})),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
frappe.bold(row.expense_account),
)
)
@@ -196,7 +230,7 @@ class AssetRepair(AccountsController):
self.cancel_sabb()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
frappe.get_lazy_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending" and self.docstatus == 1:
@@ -231,6 +265,12 @@ class AssetRepair(AccountsController):
}
)
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -242,8 +282,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -320,7 +359,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,
@@ -332,7 +372,10 @@ class AssetRepair(AccountsController):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name})
stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": self.name}, "name")
stock_entry_items = frappe.get_all(
"Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"]
)
default_expense_account = None
if not erpnext.is_perpetual_inventory_enabled(self.company):
@@ -342,7 +385,7 @@ class AssetRepair(AccountsController):
if not default_expense_account:
frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company))
for item in stock_entry.items:
for item in stock_entry_items:
if flt(item.amount) > 0:
gl_entries.append(
self.get_gl_dict(
@@ -373,7 +416,7 @@ class AssetRepair(AccountsController):
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Stock Entry",
"against_voucher": stock_entry.name,
"against_voucher": stock_entry_name,
"company": self.company,
},
item=self,
@@ -411,33 +454,152 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item")
"""
Get Purchase Invoices that have expense accounts for non-stock items.
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
"""
pi = DocType("Purchase Invoice")
pi_item = DocType("Purchase Invoice Item")
item = DocType("Item")
return (
frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
.join(Item)
.on(Item.name == PurchaseInvoiceItem.item_code)
.select(PurchaseInvoice.name)
query = (
frappe.qb.from_(pi)
.join(pi_item)
.on(pi_item.parent == pi.name)
.left_join(item)
.on(item.name == pi_item.item_code)
.select(pi.name)
.distinct()
.where(
(Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0)
& (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1)
(pi.company == filters.get("company"))
& (pi.docstatus == 1)
& (pi_item.is_fixed_asset == 0)
& (pi_item.expense_account.isnotnull())
& (pi_item.expense_account != "")
& ((pi_item.item_code.isnull()) | (item.is_stock_item == 0))
)
).run(as_list=1)
)
if txt:
query = query.where(pi.name.like(f"%{txt}%"))
return query.run(as_list=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
return (
frappe.qb.from_(PurchaseInvoiceItem)
.select(PurchaseInvoiceItem.expense_account)
.distinct()
.where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice"))
).run(as_list=1)
"""
Get expense accounts for non-stock (service) items from the purchase invoice.
Used as a query function for link fields.
"""
purchase_invoice = filters.get("purchase_invoice")
if not purchase_invoice:
return []
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
return [[account] for account in expense_accounts]
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
"""
Get expense accounts for non-stock items from the purchase invoice.
"""
pi_items = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": purchase_invoice},
fields=["item_code", "expense_account", "is_fixed_asset"],
)
if not pi_items:
return []
# Get list of stock item codes from the invoice
item_codes = {item.item_code for item in pi_items if item.item_code}
stock_items = set()
if item_codes:
stock_items = set(
frappe.db.get_all(
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
)
)
expense_accounts = set()
for item in pi_items:
# Skip stock items - they use warehouse accounts
if item.item_code and item.item_code in stock_items:
continue
# Skip fixed assets - they use asset accounts
if item.is_fixed_asset:
continue
# Use expense account from Purchase Invoice Item
if item.expense_account:
expense_accounts.add(item.expense_account)
return list(expense_accounts)
@frappe.whitelist()
def get_unallocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""
Calculate the unused repair cost for a purchase invoice and expense account.
"""
if not purchase_invoice or not expense_account:
return 0.0
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair)
total_amount = get_total_expense_amount(purchase_invoice, expense_account)
return flt(total_amount - used_amount)
def get_allocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""Get the total repair cost already allocated from submitted Asset Repairs."""
asset_repair_pi = DocType("Asset Repair Purchase Invoice")
query = (
frappe.qb.from_(asset_repair_pi)
.select(Sum(asset_repair_pi.repair_cost).as_("total"))
.where(
(asset_repair_pi.purchase_invoice == purchase_invoice)
& (asset_repair_pi.expense_account == expense_account)
& (asset_repair_pi.docstatus == 1)
)
)
if exclude_asset_repair:
query = query.where(asset_repair_pi.parent != exclude_asset_repair)
result = query.run(as_dict=True)
return flt(result[0].total) if result else 0.0
def get_total_expense_amount(purchase_invoice: str, expense_account: str) -> float:
"""Get the total expense amount from GL entries for a purchase invoice and account."""
gl_entry = DocType("GL Entry")
result = (
frappe.qb.from_(gl_entry)
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
.where(
(gl_entry.voucher_type == "Purchase Invoice")
& (gl_entry.voucher_no == purchase_invoice)
& (gl_entry.account == expense_account)
& (gl_entry.is_cancelled == 0)
)
).run(as_dict=True)
return flt(result[0].total) if result else 0.0

View File

@@ -3,6 +3,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
@@ -173,6 +175,39 @@ class TestAssetRepair(IntegrationTestCase):
)
self.assertTrue(asset_repair.invoices)
def test_repair_cost_exceeds_available_amount(self):
"""Test that repair cost cannot exceed available amount from Purchase Invoice."""
asset_repair1 = create_asset_repair(
capitalize_repair_cost=1,
item="_Test Non Stock Item",
submit=1,
)
pi_name = asset_repair1.invoices[0].purchase_invoice
expense_account = asset_repair1.invoices[0].expense_account
asset_repair2 = frappe.new_doc("Asset Repair")
asset_repair2.update(
{
"asset": asset_repair1.asset,
"asset_name": asset_repair1.asset_name,
"failure_date": nowdate(),
"description": "Second Repair",
"company": asset_repair1.company,
"capitalize_repair_cost": 1,
}
)
asset_repair2.append(
"invoices",
{
"purchase_invoice": pi_name,
"expense_account": expense_account,
"repair_cost": 10, # PI already fully used, so this should fail
},
)
self.assertRaises(frappe.ValidationError, asset_repair2.save)
def test_gl_entries_with_perpetual_inventory(self):
set_depreciation_settings_in_company(company="_Test Company with perpetual inventory")
@@ -322,6 +357,31 @@ class TestAssetRepair(IntegrationTestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations + (

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

@@ -186,7 +186,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:
@@ -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
@@ -397,6 +379,9 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -574,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:
@@ -683,7 +659,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
grand_total_diff = self.grand_total_diff
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -924,12 +900,11 @@ class calculate_taxes_and_totals:
)
)
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

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

@@ -1478,6 +1478,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
items = {}
for d in bom.get(table):
# Phantom item is exploded, so its cost is considered via its components
if d.get("is_phantom_item"):
continue
items.setdefault(d.item_code, d.amount)
non_stock_items = frappe.get_all(

View File

@@ -7,6 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, flt, sbool
from pypika.terms import ValueWrapper
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
@@ -373,7 +374,7 @@ def get_children(doctype=None, parent=None, **kwargs):
"parent as parent_id",
"qty",
"idx",
"'BOM Creator Item' as doctype",
ValueWrapper("BOM Creator Item").as_("doctype"),
"name",
"uom",
"rate",

View File

@@ -3271,6 +3271,199 @@ class TestWorkOrder(IntegrationTestCase):
)
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve)
def test_phantom_bom_item_not_in_additional_cost(self):
"""Test that phantom BOMs are not added to additional costs,
but regular non-stock items in the FG BOM are added."""
from erpnext.stock.doctype.item.test_item import make_item
# Create items:
# - FG Item (stock item)
# - Phantom sub-assembly (non-stock item to be phantom)
# - Phantom RM (stock item - component of phantom BOM)
# - Packing Material (non-stock item - directly in FG BOM)
# - Regular RM (stock item - directly in FG BOM)
fg_item = make_item(
"Test FG Item For Phantom Non Stock",
{"is_stock_item": 1, "valuation_rate": 100},
).name
phantom_item = make_item(
"Test Phantom Sub Assembly Non Stock",
{"is_stock_item": 0, "valuation_rate": 0},
).name
phantom_rm = make_item(
"Test Phantom RM Item",
{"is_stock_item": 1, "valuation_rate": 200},
).name
packing_material = make_item(
"Test Packing Material Non Stock",
{"is_stock_item": 0, "valuation_rate": 150},
).name
regular_rm = make_item(
"Test Regular RM Stock Item",
{"is_stock_item": 1, "valuation_rate": 100},
).name
# Create price list entries for non-stock items
price_list = "_Test Price List India"
for item_code, rate in [
(phantom_item, 500),
(phantom_rm, 200),
(packing_material, 150),
]:
if not frappe.db.get_value("Item Price", {"item_code": item_code, "price_list": price_list}):
frappe.get_doc(
{
"doctype": "Item Price",
"item_code": item_code,
"price_list_rate": rate,
"price_list": price_list,
}
).insert(ignore_permissions=True)
# Create Phantom BOM (for the phantom sub-assembly)
phantom_bom = frappe.get_doc(
{
"doctype": "BOM",
"item": phantom_item,
"is_default": 1,
"is_active": 1,
"is_phantom_bom": 1, # Mark as phantom BOM
"currency": "INR",
"quantity": 1,
"company": "_Test Company",
"rm_cost_as_per": "Price List",
"buying_price_list": price_list,
}
)
phantom_bom.append(
"items",
{
"item_code": phantom_rm,
"qty": 1,
"rate": 200,
},
)
phantom_bom.insert()
phantom_bom.submit()
# Create FG BOM with phantom item, packing material, and regular RM
fg_bom = frappe.get_doc(
{
"doctype": "BOM",
"item": fg_item,
"is_default": 1,
"is_active": 1,
"currency": "INR",
"quantity": 1,
"company": "_Test Company",
"rm_cost_as_per": "Price List",
"buying_price_list": price_list,
}
)
# Add phantom item (will be marked as is_phantom_item based on is_phantom_bom)
fg_bom.append(
"items",
{
"item_code": phantom_item,
"qty": 1,
"rate": 200,
"bom_no": phantom_bom.name,
},
)
# Add packing material (non-stock, directly in FG BOM)
fg_bom.append(
"items",
{
"item_code": packing_material,
"qty": 1,
"rate": 150,
},
)
# Add regular RM (stock item)
fg_bom.append(
"items",
{
"item_code": regular_rm,
"qty": 1,
"rate": 100,
},
)
fg_bom.insert()
fg_bom.submit()
# Ensure stock
test_stock_entry.make_stock_entry(
item_code=regular_rm,
target="_Test Warehouse - _TC",
qty=10,
basic_rate=100,
)
test_stock_entry.make_stock_entry(
item_code=phantom_rm,
target="_Test Warehouse - _TC",
qty=10,
basic_rate=200,
)
# Create work order
wo = make_wo_order_test_record(
production_item=fg_item,
bom_no=fg_bom.name,
qty=1,
source_warehouse="_Test Warehouse - _TC",
)
# Transfer materials
se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
se_transfer.insert()
se_transfer.submit()
# Manufacture
se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
se_manufacture.insert()
# Verify additional costs
self.assertTrue(se_manufacture.additional_costs, "Additional costs should not be empty")
total_additional_cost = sum(row.amount for row in se_manufacture.additional_costs)
self.assertEqual(
total_additional_cost,
150, # only packing material; phantom RM excluded
f"Additional cost should be 150 (packing material only), got {total_additional_cost}",
)
self.assertEqual(
se_manufacture.total_outgoing_value,
300, # 100 (regular RM) + 200 (phantom RM)
f"Total outgoing value should be 300, got {se_manufacture.total_outgoing_value}",
)
self.assertEqual(
se_manufacture.total_incoming_value,
450, # 300 (RM total) + 150 (packing material)
f"Total incoming value should be 450, got {se_manufacture.total_incoming_value}",
)
# Clean up
se_manufacture.submit()
se_manufacture.cancel()
se_transfer.cancel()
wo.reload()
wo.cancel()
fg_bom.cancel()
phantom_bom.cancel()
def test_phantom_bom_explosion(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests

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
@@ -449,3 +448,11 @@ erpnext.patches.v16_0.set_company_wise_warehouses
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,4 +1,5 @@
import frappe
from pypika.terms import ValueWrapper
from erpnext.accounts.general_ledger import make_reverse_gl_entries
@@ -39,7 +40,7 @@ def execute():
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
ValueWrapper(0).as_("paid_amount"),
"discounted_amount",
],
)

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,11 @@
import frappe
from frappe.utils import flt
def execute():
budgets = frappe.get_all("Budget", filters={"docstatus": ["in", [0, 1]]}, fields=["name"])
for b in budgets:
doc = frappe.get_doc("Budget", b.name)
total = sum(flt(row.amount) for row in doc.budget_distribution)
doc.db_set("budget_distribution_total", total, update_modified=False)

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

@@ -383,7 +383,7 @@ def get_timesheet_data(name, project):
data = frappe.get_all(
"Timesheet",
fields=[
"(total_billable_amount - total_billed_amount) as billing_amt",
{"SUB": ["total_billable_amount", "total_billed_amount"], "as": "billing_amt"},
"total_billable_hours as billing_hours",
],
filters={"name": name},

View File

@@ -380,6 +380,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
// reset value from earlier calculations
this.grand_total_diff = 0;
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
@@ -617,6 +620,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (diff && Math.abs(diff) <= 5.0 / Math.pow(10, precision("tax_amount", last_tax))) {
me.grand_total_diff = diff;
} else {
me.grand_total_diff = 0;
}
}
}
@@ -626,7 +631,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
const grand_total_diff = this.grand_total_diff;
this.frm.doc.grand_total = flt(
tax_count ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total

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

@@ -5,6 +5,7 @@
import json
import frappe
from frappe.query_builder import DocType, Order
from frappe.utils import cint, get_datetime
from frappe.utils.nestedset import get_root_of
@@ -200,18 +201,24 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
for item in items_data:
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
"valid_from": ["<=", current_date],
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
)
ItemPrice = DocType("Item Price")
item_prices = (
frappe.qb.from_(ItemPrice)
.select(
ItemPrice.price_list_rate,
ItemPrice.currency,
ItemPrice.uom,
ItemPrice.batch_no,
ItemPrice.valid_from,
ItemPrice.valid_upto,
)
.where(ItemPrice.price_list == price_list)
.where(ItemPrice.item_code == item.item_code)
.where(ItemPrice.selling == 1)
.where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull()))
.where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull()))
.orderby(ItemPrice.valid_from, order=Order.desc)
).run(as_dict=True)
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
item_uom = item.stock_uom

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,