From 9a499eb6c119b5b568facf253c02f39d52aeeea2 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Fri, 22 May 2026 17:32:19 +0530 Subject: [PATCH 1/7] refactor: calculate income tax slab method in salary slip to inject surcharge calculation with marginal relief for india region --- .../income_tax_slab/income_tax_slab.py | 96 ++++++++++++++++++- .../doctype/salary_slip/salary_slip.py | 71 +------------- .../income_tax_computation.py | 2 +- 3 files changed, 97 insertions(+), 72 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py index 372afe9736..fe1ce9e7b9 100644 --- a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -1,12 +1,17 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from datetime import date +import frappe +from frappe import _ from frappe.model.document import Document +from frappe.utils import cstr, flt, get_first_day, get_last_day, getdate -# import frappe import erpnext +from hrms.hr.utils import calculate_tax_with_marginal_relief + class IncomeTaxSlab(Document): # begin: auto-generated types @@ -37,3 +42,92 @@ class IncomeTaxSlab(Document): def validate(self): if self.company: self.currency = erpnext.get_company_currency(self.company) + + +def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None): + if annual_taxable_earning <= tax_slab.tax_relief_limit: + return 0, 0 + + tax_amount = calculate_base_tax_from_tax_slabs( + tax_slab, annual_taxable_earning, eval_globals, eval_locals + ) + + if tax_with_marginal_relief := calculate_tax_with_marginal_relief( + tax_slab, tax_amount, annual_taxable_earning + ): + tax_amount = tax_with_marginal_relief + + tax_amount, surcharge = apply_surcharge_with_marginal_relief( + tax_amount, annual_taxable_earning, tax_slab, eval_globals, eval_locals + ) + + tax_amount, other_taxes = calculate_other_charges(tax_slab, annual_taxable_earning, tax_amount) + return tax_amount, surcharge + other_taxes + + +def calculate_base_tax_from_tax_slabs(tax_slab, annual_taxable_earning, eval_globals, eval_locals): + tax_amount = 0 + eval_locals.update({"annual_taxable_earning": annual_taxable_earning}) + + for slab in tax_slab.slabs: + cond = cstr(slab.condition).strip() + if cond and not eval_tax_slab_condition(cond, eval_globals, eval_locals): + continue + if not slab.to_amount and annual_taxable_earning >= slab.from_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 + continue + if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 + elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 + return tax_amount + + +def calculate_other_charges(tax_slab, annual_taxable_earning, tax_amount): + total_other_taxes_and_charges = 0 + for d in tax_slab.other_taxes_and_charges: + if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning: + continue + + if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning: + continue + other_taxes_and_charges = tax_amount * flt(d.percent) / 100 + tax_amount += other_taxes_and_charges + total_other_taxes_and_charges += other_taxes_and_charges + + return tax_amount, total_other_taxes_and_charges + + +def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): + if not eval_globals: + eval_globals = { + "int": int, + "float": float, + "long": int, + "round": round, + "date": date, + "getdate": getdate, + "get_first_day": get_first_day, + "get_last_day": get_last_day, + } + try: + condition = condition.strip() + if condition: + return frappe.safe_eval(condition, eval_globals, eval_locals) + except NameError as err: + frappe.throw( + _("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) + except SyntaxError as err: + frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(err)) + except Exception as e: + frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) + raise + + +@erpnext.allow_regional +def apply_surcharge_with_marginal_relief( + tax_amount, annual_taxable_earning, tax_slab, eval_globals, eval_locals +): + return tax_amount, 0 diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index c11f951799..1fe9d09e02 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -40,6 +40,7 @@ create_employee_benefit_ledger_entry, delete_employee_benefit_ledger_entry, ) +from hrms.payroll.doctype.income_tax_slab.income_tax_slab import calculate_tax_by_tax_slab from hrms.payroll.doctype.payroll_entry.payroll_entry import get_salary_withholdings, get_start_end_dates from hrms.payroll.doctype.payroll_period.payroll_period import ( get_payroll_period, @@ -2561,76 +2562,6 @@ def get_payroll_payable_account(company, payroll_entry): return payroll_payable_account -def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None): - from hrms.hr.utils import calculate_tax_with_marginal_relief - - tax_amount = 0 - total_other_taxes_and_charges = 0 - - if annual_taxable_earning > tax_slab.tax_relief_limit: - eval_locals.update({"annual_taxable_earning": annual_taxable_earning}) - - for slab in tax_slab.slabs: - cond = cstr(slab.condition).strip() - if cond and not eval_tax_slab_condition(cond, eval_globals, eval_locals): - continue - if not slab.to_amount and annual_taxable_earning >= slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 - continue - - if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 - elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 - - tax_with_marginal_relief = calculate_tax_with_marginal_relief( - tax_slab, tax_amount, annual_taxable_earning - ) - if tax_with_marginal_relief is not None: - tax_amount = tax_with_marginal_relief - - for d in tax_slab.other_taxes_and_charges: - if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning: - continue - - if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning: - continue - other_taxes_and_charges = tax_amount * flt(d.percent) / 100 - tax_amount += other_taxes_and_charges - total_other_taxes_and_charges += other_taxes_and_charges - - return tax_amount, total_other_taxes_and_charges - - -def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): - if not eval_globals: - eval_globals = { - "int": int, - "float": float, - "long": int, - "round": round, - "date": date, - "getdate": getdate, - "get_first_day": get_first_day, - "get_last_day": get_last_day, - } - - try: - condition = condition.strip() - if condition: - return frappe.safe_eval(condition, eval_globals, eval_locals) - except NameError as err: - frappe.throw( - _("{0}
This error can be due to missing or deleted field.").format(err), - title=_("Name error"), - ) - except SyntaxError as err: - frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(err)) - except Exception as e: - frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) - raise - - def get_lwp_or_ppl_for_date_range(employee, start_date, end_date): LeaveApplication = frappe.qb.DocType("Leave Application") LeaveType = frappe.qb.DocType("Leave Type") diff --git a/hrms/payroll/report/income_tax_computation/income_tax_computation.py b/hrms/payroll/report/income_tax_computation/income_tax_computation.py index 607eb6415d..e45f516748 100644 --- a/hrms/payroll/report/income_tax_computation/income_tax_computation.py +++ b/hrms/payroll/report/income_tax_computation/income_tax_computation.py @@ -6,8 +6,8 @@ from frappe.query_builder.functions import Sum from frappe.utils import add_days, flt, getdate, rounded +from hrms.payroll.doctype.income_tax_slab.income_tax_slab import calculate_tax_by_tax_slab from hrms.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates -from hrms.payroll.doctype.salary_slip.salary_slip import calculate_tax_by_tax_slab def execute(filters=None): From 79d7eef59fe300ced4ddb356b38e71c9233be583 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sat, 23 May 2026 16:40:24 +0530 Subject: [PATCH 2/7] chore: wrap exception error in str() --- hrms/payroll/doctype/income_tax_slab/income_tax_slab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py index fe1ce9e7b9..67187c26d9 100644 --- a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -116,13 +116,13 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): return frappe.safe_eval(condition, eval_globals, eval_locals) except NameError as err: frappe.throw( - _("{0}
This error can be due to missing or deleted field.").format(err), + _("{0}
This error can be due to missing or deleted field.").format(str(err)), title=_("Name error"), ) except SyntaxError as err: - frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(err)) + frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(str(err))) except Exception as e: - frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) + frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(str(e))) raise From e7d076878a5d63a2c71e20d75d7e827cc720c25d Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sat, 23 May 2026 16:49:28 +0530 Subject: [PATCH 3/7] test: test calculate income tax methods --- .../income_tax_slab/test_income_tax_slab.py | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py index fb63f42abd..23ab7d0ac4 100644 --- a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py @@ -1,9 +1,117 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe + +from hrms.payroll.doctype.income_tax_slab.income_tax_slab import ( + calculate_base_tax_from_tax_slabs, + calculate_other_charges, + calculate_tax_by_tax_slab, +) from hrms.tests.utils import HRMSTestSuite +def make_slab(slabs, other_taxes_and_charges=None, tax_relief_limit=0): + return frappe._dict( + slabs=[frappe._dict(**s) for s in slabs], + surcharge_slabs=[], + other_taxes_and_charges=[frappe._dict(**c) for c in (other_taxes_and_charges or [])], + tax_relief_limit=tax_relief_limit, + ) + + +SIMPLE_SLABS = [ + {"from_amount": 0, "to_amount": 300000, "percent_deduction": 0, "condition": ""}, + {"from_amount": 300000, "to_amount": 700000, "percent_deduction": 5, "condition": ""}, + {"from_amount": 700000, "to_amount": 1000000, "percent_deduction": 10, "condition": ""}, + {"from_amount": 1000000, "to_amount": 0, "percent_deduction": 30, "condition": ""}, +] + + class TestIncomeTaxSlab(HRMSTestSuite): - pass + def setUp(self): + self.slab = make_slab(SIMPLE_SLABS) + self.slab_with_cess = make_slab( + SIMPLE_SLABS, + other_taxes_and_charges=[ + {"description": "cess", "percent": 4, "min_taxable_income": 0, "max_taxable_income": 0} + ], + ) + + # calculate_other_charges + + def test_other_charges_applied_as_flat_rate(self): + """Charges in other_taxes_and_charges are a flat percentage — no marginal relief logic.""" + base_tax = calculate_base_tax_from_tax_slabs(self.slab_with_cess, 1500000, None, {}) + + _, cess = calculate_other_charges(self.slab_with_cess, 1500000, base_tax) + + self.assertEqual(cess, base_tax * 4 / 100) + + def test_other_charges_no_marginal_relief_at_threshold(self): + """A surcharge row in other_taxes_and_charges at an income where marginal relief + would normally apply is still computed as a plain percentage — no relief reduction.""" + # Income just above 50L — where marginal relief would apply if using surcharge_slabs + income = 5100000 + slab = make_slab( + SIMPLE_SLABS, + other_taxes_and_charges=[ + { + "description": "Surcharge 10%", + "percent": 10, + "min_taxable_income": 0, + "max_taxable_income": 0, + } + ], + ) + base_tax = calculate_base_tax_from_tax_slabs(slab, income, None, {}) + + _, surcharge = calculate_other_charges(slab, income, base_tax) + + self.assertEqual(surcharge, base_tax * 10 / 100) + + def test_other_charges_skipped_outside_income_range(self): + """Charges with min/max taxable income bounds are skipped when income is outside the range.""" + slab = make_slab( + SIMPLE_SLABS, + other_taxes_and_charges=[ + { + "description": "High income cess", + "percent": 4, + "min_taxable_income": 10000000, + "max_taxable_income": 0, + } + ], + ) + base_tax = calculate_base_tax_from_tax_slabs(slab, 1500000, None, {}) + + _, charge = calculate_other_charges(slab, 1500000, base_tax) + + self.assertEqual(charge, 0) + + # calculate_tax_by_tax_slab + + def test_zero_tax_below_relief_limit(self): + """Income at or below tax_relief_limit returns zero tax.""" + slab = make_slab(SIMPLE_SLABS, tax_relief_limit=700000) + + tax, _ = calculate_tax_by_tax_slab(700000, slab, None, {}) + + self.assertEqual(tax, 0) + + def test_tax_calculated_above_relief_limit(self): + """Income above tax_relief_limit produces non-zero tax.""" + slab = make_slab(SIMPLE_SLABS, tax_relief_limit=700000) + + tax, _ = calculate_tax_by_tax_slab(800000, slab, None, {}) + + self.assertGreater(tax, 0) + + def test_cess_computed_on_base_tax(self): + """other_taxes_and_charges (cess) is computed on the running tax total passed in.""" + base_tax = calculate_base_tax_from_tax_slabs(self.slab_with_cess, 1500000, None, {}) + total_tax, charges = calculate_tax_by_tax_slab(1500000, self.slab_with_cess, None, {}) + + expected_cess = base_tax * 4 / 100 + self.assertEqual(charges, expected_cess) + self.assertEqual(total_tax, base_tax + expected_cess) From 25fa258c96c8bee34e99cd878bada9849cc3fd59 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sat, 23 May 2026 17:00:53 +0530 Subject: [PATCH 4/7] test: integration test for income tax without marginal relief --- .../income_tax_slab/test_income_tax_slab.py | 105 +++++++++++++++++- 1 file changed, 99 insertions(+), 6 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py index 23ab7d0ac4..8e86558ea3 100644 --- a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py @@ -3,13 +3,27 @@ import frappe +from erpnext.setup.doctype.employee.test_employee import make_employee + +from hrms.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( + create_payroll_period, +) from hrms.payroll.doctype.income_tax_slab.income_tax_slab import ( calculate_base_tax_from_tax_slabs, calculate_other_charges, calculate_tax_by_tax_slab, ) +from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from hrms.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from hrms.tests.utils import HRMSTestSuite +SIMPLE_SLABS = [ + {"from_amount": 0, "to_amount": 300000, "percent_deduction": 0, "condition": ""}, + {"from_amount": 300000, "to_amount": 700000, "percent_deduction": 5, "condition": ""}, + {"from_amount": 700000, "to_amount": 1000000, "percent_deduction": 10, "condition": ""}, + {"from_amount": 1000000, "to_amount": 0, "percent_deduction": 30, "condition": ""}, +] + def make_slab(slabs, other_taxes_and_charges=None, tax_relief_limit=0): return frappe._dict( @@ -30,8 +44,8 @@ def make_slab(slabs, other_taxes_and_charges=None, tax_relief_limit=0): class TestIncomeTaxSlab(HRMSTestSuite): def setUp(self): - self.slab = make_slab(SIMPLE_SLABS) - self.slab_with_cess = make_slab( + self.slab = make_income_tax_slab(SIMPLE_SLABS) + self.slab_with_cess = make_income_tax_slab( SIMPLE_SLABS, other_taxes_and_charges=[ {"description": "cess", "percent": 4, "min_taxable_income": 0, "max_taxable_income": 0} @@ -53,7 +67,7 @@ def test_other_charges_no_marginal_relief_at_threshold(self): would normally apply is still computed as a plain percentage — no relief reduction.""" # Income just above 50L — where marginal relief would apply if using surcharge_slabs income = 5100000 - slab = make_slab( + slab = make_income_tax_slab( SIMPLE_SLABS, other_taxes_and_charges=[ { @@ -72,7 +86,7 @@ def test_other_charges_no_marginal_relief_at_threshold(self): def test_other_charges_skipped_outside_income_range(self): """Charges with min/max taxable income bounds are skipped when income is outside the range.""" - slab = make_slab( + slab = make_income_tax_slab( SIMPLE_SLABS, other_taxes_and_charges=[ { @@ -93,7 +107,7 @@ def test_other_charges_skipped_outside_income_range(self): def test_zero_tax_below_relief_limit(self): """Income at or below tax_relief_limit returns zero tax.""" - slab = make_slab(SIMPLE_SLABS, tax_relief_limit=700000) + slab = make_income_tax_slab(SIMPLE_SLABS, tax_relief_limit=700000) tax, _ = calculate_tax_by_tax_slab(700000, slab, None, {}) @@ -101,7 +115,7 @@ def test_zero_tax_below_relief_limit(self): def test_tax_calculated_above_relief_limit(self): """Income above tax_relief_limit produces non-zero tax.""" - slab = make_slab(SIMPLE_SLABS, tax_relief_limit=700000) + slab = make_income_tax_slab(SIMPLE_SLABS, tax_relief_limit=700000) tax, _ = calculate_tax_by_tax_slab(800000, slab, None, {}) @@ -115,3 +129,82 @@ def test_cess_computed_on_base_tax(self): expected_cess = base_tax * 4 / 100 self.assertEqual(charges, expected_cess) self.assertEqual(total_tax, base_tax + expected_cess) + + def test_surcharge_then_cess_reflected_in_salary_slip_tds(self): + """Integration: TDS on a salary slip equals (base_tax + 10% surcharge + 4% cess) / 12. + Surcharge is applied first, so cess base is base_tax + surcharge — not base_tax alone.""" + income_tax_slab = frappe.get_doc( + { + "doctype": "Income Tax Slab", + "name": "_Test Slab Surcharge Cess", + "effective_from": "2024-04-01", + "currency": "INR", + "tax_relief_limit": 0, + "slabs": [ + {"from_amount": 0, "to_amount": 300000, "percent_deduction": 0}, + {"from_amount": 300000, "to_amount": 700000, "percent_deduction": 5}, + {"from_amount": 700000, "to_amount": 1000000, "percent_deduction": 10}, + {"from_amount": 1000000, "to_amount": 0, "percent_deduction": 30}, + ], + "other_taxes_and_charges": [ + { + "description": "Surcharge 10%", + "percent": 10, + "min_taxable_income": 5000000, + "max_taxable_income": 0, + }, + { + "description": "Cess 4%", + "percent": 4, + "min_taxable_income": 0, + "max_taxable_income": 0, + }, + ], + } + ) + income_tax_slab.insert(ignore_permissions=True) + income_tax_slab.submit() + + payroll_period = create_payroll_period( + name="_Test Payroll Period Surcharge Cess", company="_Test Company" + ) + employee = make_employee("test_surcharge_cess@salary.slip", company="_Test Company") + + salary_structure = make_salary_structure( + "Structure for Surcharge Cess Test", + "Monthly", + test_tax=True, + employee=employee, + payroll_period=payroll_period, + company="_Test Company", + base=600000, # ₹7.2 M annual — in 10% surcharge band (min_taxable_income: 50L) + ) + + ssa_name = frappe.db.get_value("Salary Structure Assignment", {"employee": employee, "docstatus": 1}) + frappe.db.set_value("Salary Structure Assignment", ssa_name, "income_tax_slab", income_tax_slab.name) + + # Post on the first day of the payroll period → remaining_sub_periods = 12 + salary_slip = make_salary_slip( + salary_structure.name, + employee=employee, + posting_date=payroll_period.start_date, + ) + + tds = next((d.amount for d in salary_slip.deductions if d.salary_component == "TDS"), 0) + + annual_income = salary_slip.annual_taxable_amount + base_tax = calculate_base_tax_from_tax_slabs(income_tax_slab, annual_income, None, {}) + surcharge = base_tax * 10 / 100 + cess = (base_tax + surcharge) * 4 / 100 + expected_tds = round((base_tax + surcharge + cess) / 12) + + self.assertEqual(tds, expected_tds) + + +def make_income_tax_slab(slabs, other_taxes_and_charges=None, tax_relief_limit=0): + return frappe._dict( + slabs=[frappe._dict(**s) for s in slabs], + surcharge_slabs=[], + other_taxes_and_charges=[frappe._dict(**c) for c in (other_taxes_and_charges or [])], + tax_relief_limit=tax_relief_limit, + ) From 78e529d84a0f8b969d9430fc32441738075222ac Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sat, 23 May 2026 18:47:03 +0530 Subject: [PATCH 5/7] chore: clean up after conflicts --- .../income_tax_slab/test_income_tax_slab.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py index 8e86558ea3..32fe4453fd 100644 --- a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py @@ -25,23 +25,6 @@ ] -def make_slab(slabs, other_taxes_and_charges=None, tax_relief_limit=0): - return frappe._dict( - slabs=[frappe._dict(**s) for s in slabs], - surcharge_slabs=[], - other_taxes_and_charges=[frappe._dict(**c) for c in (other_taxes_and_charges or [])], - tax_relief_limit=tax_relief_limit, - ) - - -SIMPLE_SLABS = [ - {"from_amount": 0, "to_amount": 300000, "percent_deduction": 0, "condition": ""}, - {"from_amount": 300000, "to_amount": 700000, "percent_deduction": 5, "condition": ""}, - {"from_amount": 700000, "to_amount": 1000000, "percent_deduction": 10, "condition": ""}, - {"from_amount": 1000000, "to_amount": 0, "percent_deduction": 30, "condition": ""}, -] - - class TestIncomeTaxSlab(HRMSTestSuite): def setUp(self): self.slab = make_income_tax_slab(SIMPLE_SLABS) From 32f1307b14a0c69be5d864e92b71ce2781f86a99 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sat, 23 May 2026 19:16:47 +0530 Subject: [PATCH 6/7] chore: initialise empty dict chore: fix flaky test --- hrms/hr/doctype/attendance_request/test_attendance_request.py | 1 + hrms/payroll/doctype/income_tax_slab/income_tax_slab.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/hrms/hr/doctype/attendance_request/test_attendance_request.py b/hrms/hr/doctype/attendance_request/test_attendance_request.py index 9c91208ff3..f26871bd83 100644 --- a/hrms/hr/doctype/attendance_request/test_attendance_request.py +++ b/hrms/hr/doctype/attendance_request/test_attendance_request.py @@ -165,6 +165,7 @@ def get_attendance_records(self, attendance_request: str) -> list[dict]: ) def test_validate_no_attendance_to_create(self): + frappe.db.delete("Holiday", {"parent": self.holiday_list}) today = getdate() yesterday = add_days(today, -1) # marking absent for two days diff --git a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py index 67187c26d9..8f8a5522b8 100644 --- a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -45,6 +45,9 @@ def validate(self): def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None): + eval_globals = eval_globals or {} + eval_locals = eval_locals or {} + if annual_taxable_earning <= tax_slab.tax_relief_limit: return 0, 0 From 25b4dd78d2f8151387d8b76c5e1cf41e033ec4c1 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 25 May 2026 15:55:39 +0530 Subject: [PATCH 7/7] chore: function name clean up --- .../doctype/income_tax_slab/income_tax_slab.py | 8 ++++---- .../income_tax_slab/test_income_tax_slab.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py index 8f8a5522b8..3569225703 100644 --- a/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -52,7 +52,7 @@ def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=Non return 0, 0 tax_amount = calculate_base_tax_from_tax_slabs( - tax_slab, annual_taxable_earning, eval_globals, eval_locals + annual_taxable_earning, tax_slab, eval_globals, eval_locals ) if tax_with_marginal_relief := calculate_tax_with_marginal_relief( @@ -64,11 +64,11 @@ def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=Non tax_amount, annual_taxable_earning, tax_slab, eval_globals, eval_locals ) - tax_amount, other_taxes = calculate_other_charges(tax_slab, annual_taxable_earning, tax_amount) + tax_amount, other_taxes = calculate_other_charges(tax_amount, annual_taxable_earning, tax_slab) return tax_amount, surcharge + other_taxes -def calculate_base_tax_from_tax_slabs(tax_slab, annual_taxable_earning, eval_globals, eval_locals): +def calculate_base_tax_from_tax_slabs(annual_taxable_earning, tax_slab, eval_globals, eval_locals): tax_amount = 0 eval_locals.update({"annual_taxable_earning": annual_taxable_earning}) @@ -86,7 +86,7 @@ def calculate_base_tax_from_tax_slabs(tax_slab, annual_taxable_earning, eval_glo return tax_amount -def calculate_other_charges(tax_slab, annual_taxable_earning, tax_amount): +def calculate_other_charges(tax_amount, annual_taxable_earning, tax_slab): total_other_taxes_and_charges = 0 for d in tax_slab.other_taxes_and_charges: if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning: diff --git a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py index 32fe4453fd..3612b10ed6 100644 --- a/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py +++ b/hrms/payroll/doctype/income_tax_slab/test_income_tax_slab.py @@ -39,9 +39,9 @@ def setUp(self): def test_other_charges_applied_as_flat_rate(self): """Charges in other_taxes_and_charges are a flat percentage — no marginal relief logic.""" - base_tax = calculate_base_tax_from_tax_slabs(self.slab_with_cess, 1500000, None, {}) + base_tax = calculate_base_tax_from_tax_slabs(1500000, self.slab_with_cess, None, {}) - _, cess = calculate_other_charges(self.slab_with_cess, 1500000, base_tax) + _, cess = calculate_other_charges(base_tax, 1500000, self.slab_with_cess) self.assertEqual(cess, base_tax * 4 / 100) @@ -61,9 +61,9 @@ def test_other_charges_no_marginal_relief_at_threshold(self): } ], ) - base_tax = calculate_base_tax_from_tax_slabs(slab, income, None, {}) + base_tax = calculate_base_tax_from_tax_slabs(income, slab, None, {}) - _, surcharge = calculate_other_charges(slab, income, base_tax) + _, surcharge = calculate_other_charges(base_tax, income, slab) self.assertEqual(surcharge, base_tax * 10 / 100) @@ -80,9 +80,9 @@ def test_other_charges_skipped_outside_income_range(self): } ], ) - base_tax = calculate_base_tax_from_tax_slabs(slab, 1500000, None, {}) + base_tax = calculate_base_tax_from_tax_slabs(1500000, slab, None, {}) - _, charge = calculate_other_charges(slab, 1500000, base_tax) + _, charge = calculate_other_charges(base_tax, 1500000, slab) self.assertEqual(charge, 0) @@ -106,7 +106,7 @@ def test_tax_calculated_above_relief_limit(self): def test_cess_computed_on_base_tax(self): """other_taxes_and_charges (cess) is computed on the running tax total passed in.""" - base_tax = calculate_base_tax_from_tax_slabs(self.slab_with_cess, 1500000, None, {}) + base_tax = calculate_base_tax_from_tax_slabs(1500000, self.slab_with_cess, None, {}) total_tax, charges = calculate_tax_by_tax_slab(1500000, self.slab_with_cess, None, {}) expected_cess = base_tax * 4 / 100 @@ -176,7 +176,7 @@ def test_surcharge_then_cess_reflected_in_salary_slip_tds(self): tds = next((d.amount for d in salary_slip.deductions if d.salary_component == "TDS"), 0) annual_income = salary_slip.annual_taxable_amount - base_tax = calculate_base_tax_from_tax_slabs(income_tax_slab, annual_income, None, {}) + base_tax = calculate_base_tax_from_tax_slabs(annual_income, income_tax_slab, None, {}) surcharge = base_tax * 10 / 100 cess = (base_tax + surcharge) * 4 / 100 expected_tds = round((base_tax + surcharge + cess) / 12)