Source code for taxcalcindia.calculator

from .models import SalaryIncome,CapitalGainsIncome,BusinessIncome,OtherIncome,Deductions,TaxSettings,EmploymentType
from .slabs import get_tax_slabs
import pprint
import math


NEW_REGIME_KEY="new_regime"
OLD_REGIME_GEN_KEY="old_regime_general"
OLD_REGIME_SEN_KEY="old_regime_senior"
OLD_REGIME_SUPER_SEN_KEY="old_regime_super_senior"

NEW_REGIME_STANDARD_DEDUCTION=75000
OLD_REGIME_STANDARD_DEDUCTION=50000

NEW_TAX_REGIME_REBATE_LIMIT=1200000
OLD_TAX_REGIME_REBATE_LIMIT=250000
NEW_TAX_REGIME_REBATE=60000
OLD_TAX_REGIME_REBATE=12500

[docs] class IncomeTaxCalculator: """Income Tax Calculator for individuals. """
[docs] def __init__( self,settings: TaxSettings, salary: SalaryIncome | None = None, capital_gains: CapitalGainsIncome | None = None, business: BusinessIncome | None = None, other_income: OtherIncome | None = None, deductions: Deductions | None = None ): """Income Tax Calculator for individuals. """ self._validate_inputs( settings, salary, capital_gains, business, other_income, deductions ) self.settings=settings # preserve whether a salary component was explicitly provided self._has_salary = salary is not None self.salary=salary or SalaryIncome() self.capital_gains=capital_gains or CapitalGainsIncome() self._has_business_income = business is not None self.business=business or BusinessIncome() self._has_other_income = other_income is not None self.other_income=other_income or OtherIncome() self.deductions=deductions or Deductions() # cache keyed by (is_comparision_needed, is_tax_per_slab_needed, display_result) self._tax_result_cache: dict[tuple, dict] = {}
def _validate_inputs( self, settings, salary, capital_gains, business, other_income, deductions ): """Validate input parameters for the tax calculator. Args: settings (TaxSettings): Tax settings for the individual. salary (SalaryIncome | None): Salary income details. business (BusinessIncome | None): Business income details. other_income (OtherIncome | None): Other income details. deductions (Deductions | None): Deduction details. Raises: TypeError: If any of the input parameters are of the wrong type. ValueError: If no income source is provided. TypeError: If salary is not a SalaryIncome object. TypeError: If capital_gains is not a CapitalGainsIncome object. TypeError: If business is not a BusinessIncome object. TypeError: If other_income is not an OtherIncome object. TypeError: If deductions is not a Deductions object. """ if not isinstance(settings, TaxSettings): raise TypeError("settings must be TaxSettings object") if not any([salary, business, other_income, capital_gains]): raise ValueError( "at least one income source (salary, business, capital_gains or other_income) is required" ) if salary and not isinstance(salary, SalaryIncome): raise TypeError("salary must be SalaryIncome object") if capital_gains and not isinstance(capital_gains, CapitalGainsIncome): raise TypeError("capital_gains must be CapitalGainsIncome object") if business and not isinstance(business, BusinessIncome): raise TypeError("business must be BusinessIncome object") if other_income and not isinstance(other_income, OtherIncome): raise TypeError("other_income must be OtherIncome object") if deductions and not isinstance(deductions, Deductions): raise TypeError("deductions must be Deductions object") @property def gross_income(self): """Calculate the gross income from all sources. Returns: float: The total gross income. """ return ( self.salary.total + self.business.total + self.other_income.total + self.capital_gains.total ) @property def total_deductions(self): """Calculate the total deductions for the individual.""" if self.other_income and hasattr(self.other_income,'savings_account_interest'): if self.settings.age>60: self.deductions.section_80ttb=self.other_income.savings_account_interest self.deductions.section_80tta=0 else: self.deductions.section_80tta=self.other_income.savings_account_interest self.deductions.section_80ttb=0 #section_80ddb if self.settings.age>60: self.deductions.section_80ddb=min(self.deductions.section_80ddb,100000) else: self.deductions.section_80ddb=min(self.deductions.section_80ddb,40000) #section_80ccd_2 if self.settings.employment_type == EmploymentType.GOVERNMENT: self.deductions.section_80ccd_2 = min(self.deductions.section_80ccd_2, self.salary.basic_and_da * 0.14) elif self.settings.employment_type == EmploymentType.PRIVATE: self.deductions.section_80ccd_2 = min(self.deductions.section_80ccd_2, self.salary.basic_and_da * 0.10) else: self.deductions.section_80ccd_2 = 0 return ( self.deductions.total + max(self.__calculate_hra_component_for_private(),0) + max(self.__calculate_hra_component_for_self_employed(),0) + self.deductions.section_80ddb + self.deductions.section_80ccd_2 ) def __calculate_hra_component_for_private(self): if self.settings.employment_type == EmploymentType.PRIVATE: hra = min(self.salary.hra, self.salary.basic_and_da * 0.5,self.deductions.rent_for_hra_exemption-0.1*self.salary.basic_and_da) return hra return 0 def __calculate_hra_component_for_self_employed(self): if self.settings.employment_type == EmploymentType.SELF_EMPLOYED: return min(5000,0.25*self.gross_income,0.1*self.salary.basic_and_da) return 0 def __get_taxable_income(self): if self.settings.employment_type==EmploymentType.SELF_EMPLOYED: old_regime_taxable_income=max(0, self.gross_income - self.total_deductions) new_regime_taxable_income=max(0, self.gross_income) return new_regime_taxable_income, old_regime_taxable_income if (not self._has_salary) or (getattr(self.salary, "total", 0) == 0): old_regime_taxable_income = max(0, self.gross_income - self.total_deductions) new_regime_taxable_income = max(0, self.gross_income) else: old_regime_taxable_income=max(0, self.gross_income - OLD_REGIME_STANDARD_DEDUCTION - self.total_deductions) new_regime_taxable_income=max(0, self.gross_income - NEW_REGIME_STANDARD_DEDUCTION) return new_regime_taxable_income, old_regime_taxable_income def __calculate_surcharge(self, taxable_income, regime_type, tax_amount=0): ti = float(taxable_income) def get_rate(ti_value: float, is_new: bool) -> int: if ti_value <= 5000000: return 0 if ti_value <= 10000000: return 10 if ti_value <= 20000000: return 15 if ti_value <= 50000000: return 25 return 25 if is_new else 37 old_rate = get_rate(ti, is_new=False) new_rate = get_rate(ti, is_new=True) def calc_amount(rate: int): return round(float(tax_amount) * (rate / 100.0), 2) if regime_type == "old": return { "rate_percent": old_rate, "amount": calc_amount(old_rate) } elif regime_type == "new": return { "rate_percent": new_rate, "amount": calc_amount(new_rate) } def __calculate_tax_per_slab(self,taxable_income,slab): tax = 0.0 tax_per_slab = {} if taxable_income <= 0: return tax, tax_per_slab remaining = float(taxable_income) previous_limit = 0.0 for limit, rate in slab: if remaining <= 0: break slab_end = limit if limit != float("inf") else taxable_income taxable_in_slab = min(slab_end - previous_limit, remaining) slab_tax = taxable_in_slab * rate tax += slab_tax tax_per_slab[(previous_limit, min(slab_end, taxable_income))] = slab_tax remaining -= taxable_in_slab previous_limit = limit return tax, tax_per_slab def __stringify_keys(self,obj): if isinstance(obj, dict): new = {} for k, v in obj.items(): new_key = k if isinstance(k, (str, int, float, bool, type(None))) else str(k) new[new_key] = self.__stringify_keys(v) return new if isinstance(obj, list): return [self.__stringify_keys(i) for i in obj] return obj def __prepare_capital_gains_adjustments(self, taxable_income: float, regime_type: str): """Adjust taxable income and surcharge base for capital gains and detect LTGC rebate applicability.""" ltcg_rebate_applied = False cg_special_sum = ( self.capital_gains.short_term_at_20_percent + self.capital_gains.long_term_at_12_5_percent + self.capital_gains.long_term_at_20_percent ) taxable_after = taxable_income - cg_special_sum surcharge_taxable = taxable_after if not self._has_salary and not self._has_business_income and not self._has_other_income: if regime_type == "new" and taxable_after > 400000: ltcg_rebate_applied = True elif regime_type == "old" and taxable_after > 250000: ltcg_rebate_applied = True # surcharge should consider the special-capital-gains component for standalone cases surcharge_taxable = taxable_after + cg_special_sum return taxable_after, surcharge_taxable, ltcg_rebate_applied, cg_special_sum def __add_capital_gains_tax(self, base_tax: float, regime_type: str, ltcg_rebate_applied: bool) -> float: """Add capital gains tax component to base_tax according to income type and regime.""" if not self._has_salary and not self._has_business_income and not self._has_other_income: if regime_type == "new": if self.capital_gains.short_term_at_normal < self.capital_gains.long_term_at_20_percent: total_reduction = 400000 - self.capital_gains.short_term_at_normal else: total_reduction = 400000 else: if self.capital_gains.short_term_at_normal < self.capital_gains.long_term_at_20_percent: total_reduction = 250000 - self.capital_gains.short_term_at_normal else: total_reduction = 250000 base_tax += self.capital_gains._total_capital_gains_standalone_tax( 0 if ltcg_rebate_applied else total_reduction ) else: base_tax += self.capital_gains.total_capital_gains_tax return base_tax def __finalize_tax_components(self, base_tax: float, surcharge_taxable: float, regime_type: str, apply_deduction: float = 0.0): """Apply deduction, surcharge and cess; return final components dict.""" if apply_deduction: base_tax = max(base_tax - apply_deduction, 0.0) surcharge = self.__calculate_surcharge(surcharge_taxable, regime_type, base_tax) or {"amount": 0.0, "rate_percent": 0} surcharge_amount = surcharge.get("amount", 0.0) tax_after_surcharge = base_tax + (surcharge_amount if surcharge_amount else 0.0) cess = round(tax_after_surcharge * 0.04, 2) total_tax = tax_after_surcharge + cess return { "base_tax": round(base_tax, 2), "surcharge": surcharge, "surcharge_amount": round(surcharge_amount, 2), "cess": cess, "total_tax": round(total_tax, 2) } def __compute_regime_tax(self, taxable_income: float, slab, rebate_limit: float, regime_type: str, apply_deduction: float = 0.0): """Compute tax, surcharge, and cess for a given regime. Returns a dict with components.""" # preserve rebate decision based on original taxable income (matches previous behaviour) is_income_above_rebate = taxable_income > rebate_limit # prepare capital gains adjustments (returns taxable_after and surcharge base) taxable_after, surcharge_taxable, ltcg_rebate_applied, _ = self.__prepare_capital_gains_adjustments(taxable_income, regime_type) # base tax from slabs (applies only if above rebate limit) if is_income_above_rebate: base_tax, tax_per_slab = self.__calculate_tax_per_slab(taxable_after, slab) else: base_tax, tax_per_slab = 0.0, {} # add capital gains tax component base_tax = self.__add_capital_gains_tax(base_tax, regime_type, ltcg_rebate_applied) # finalize by applying deduction, surcharge and cess final = self.__finalize_tax_components(base_tax, surcharge_taxable, regime_type, apply_deduction) # merge slab breakdown back final["tax_per_slab"] = tax_per_slab return final
[docs] def calculate_tax(self, is_comparision_needed: bool = True, is_tax_per_slab_needed: bool = False, display_result: bool = False) -> dict: """Calculate tax based on the individual's income and deductions. Args: is_comparision_needed (bool, optional): Whether to include tax regime comparison. Defaults to True. is_tax_per_slab_needed (bool, optional): Whether to include tax per slab details. Defaults to False. Returns: dict: A dictionary containing the tax calculation results. """ cache_key = (bool(is_comparision_needed), bool(is_tax_per_slab_needed), bool(display_result)) if cache_key in self._tax_result_cache: cached = self._tax_result_cache[cache_key] if display_result: pprint.pprint(cached, indent=2, sort_dicts=False) return cached slabs = get_tax_slabs(self.settings.financial_year, self.settings.age) new_taxable, old_taxable = self.__get_taxable_income() if self.settings.age >= 80: old_slab_key = OLD_REGIME_SUPER_SEN_KEY elif self.settings.age >= 60: old_slab_key = OLD_REGIME_SEN_KEY else: old_slab_key = OLD_REGIME_GEN_KEY new_result = self.__compute_regime_tax( taxable_income=new_taxable, slab=slabs[NEW_REGIME_KEY], rebate_limit=NEW_TAX_REGIME_REBATE_LIMIT, regime_type="new", apply_deduction=0.0 ) old_result = self.__compute_regime_tax( taxable_income=old_taxable, slab=slabs[old_slab_key], rebate_limit=OLD_TAX_REGIME_REBATE_LIMIT, regime_type="old", apply_deduction=0.0 ) # recommendation and savings new_tax_total = new_result["total_tax"] old_tax_total = old_result["total_tax"] if new_tax_total > old_tax_total: recommended = "old" savings = round(new_tax_total - old_tax_total) summary = f"Old tax regime results in a savings of ₹{savings} compared to the new regime" else: recommended = "new" savings = round(old_tax_total - new_tax_total) summary = f"New tax regime results in a savings of ₹{savings} compared to the old regime" recommendation = { "recommended_regime": recommended, "summary": summary, "tax_savings_amount": savings } result = { "income_summary": { "gross_income": self.gross_income, "gross_deductions": self.deductions.total, "new_regime_taxable_income": new_taxable, "old_regime_taxable_income": old_taxable }, "tax_liability": { "new_regime": { "total": math.ceil(new_tax_total), "components":{ "initial_tax": new_result["base_tax"], "surcharge": new_result["surcharge"]["amount"], "cess" : new_result["cess"] }, }, "old_regime": { "total": math.ceil(old_tax_total), "components": { "initial_tax": old_result["base_tax"], "surcharge": old_result["surcharge"]["amount"], "cess" : old_result["cess"] } } } } if is_comparision_needed: result["tax_regime_comparison"] = recommendation if is_tax_per_slab_needed: result["tax_per_slabs"] = { "new_regime": new_result["tax_per_slab"], "old_regime": old_result["tax_per_slab"] } result = self.__stringify_keys(result) self._tax_result_cache[cache_key] = result if display_result: pprint.pprint(result) return result
@property def new_regime_tax(self) -> int: """Calculates the total tax payable under the new tax regime. Returns: int: Total tax payable under the new tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["tax_liability"]["new_regime"]["total"] @property def old_regime_tax(self) -> int: """Calculates the total tax payable under the old tax regime. Returns: int: Total tax payable under the old tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["tax_liability"]["old_regime"]["total"] @property def new_regime_taxable_income(self) -> float: """Calculates the taxable income under the new tax regime. Returns: float: Taxable income under the new tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["income_summary"]["new_regime_taxable_income"] @property def old_regime_taxable_income(self) -> float: """Calculates the taxable income under the old tax regime. Returns: float: Taxable income under the old tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["income_summary"]["old_regime_taxable_income"] @property def recommended_regime(self) -> str: """Calculates the recommended tax regime based on the user's income and deductions. Returns: str: Recommended tax regime ("new" or "old"). """ result = self.calculate_tax(is_comparision_needed=True) return result["tax_regime_comparison"]["recommended_regime"] @property def tax_savings(self) -> int: """Calculates the tax savings by comparing the new and old tax regimes. Returns: int: Tax savings amount. """ result = self.calculate_tax(is_comparision_needed=True) return result["tax_regime_comparison"]["tax_savings_amount"] @property def new_regime_breakup(self) -> dict: """Calculates the tax breakup under the new tax regime. Returns: dict: Tax breakup under the new tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["tax_liability"]["new_regime"]["components"] @property def old_regime_breakup(self) -> dict: """Calculates the tax breakup under the old tax regime. Returns: dict: Tax breakup under the old tax regime. """ result = self.calculate_tax(is_comparision_needed=False) return result["tax_liability"]["old_regime"]["components"]
[docs] def tax_per_slab(self, regime: str = "new") -> dict: """Calculates the tax per slab under the specified tax regime. Args: regime (str, optional): The tax regime ("new" or "old"). Defaults to "new". Raises: ValueError: If the regime is not "new" or "old". Returns: dict: Tax per slab under the specified tax regime. """ if regime not in ("new", "old"): raise ValueError("regime must be 'new' or 'old'") result = self.calculate_tax(is_tax_per_slab_needed=True) key = "new_regime" if regime == "new" else "old_regime" return result["tax_per_slabs"][key]
[docs] def clear_cache(self) -> None: """Clear the internal tax result cache. Call this after mutating inputs.""" self._tax_result_cache.clear()