Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ frappe.query_reports["Employee CTC Break-up"] = {
},
on_change: function () {
let employee = frappe.query_report.get_filter_value("employee");
if (!employee) {
frappe.query_report.set_filter_value("salary_structure_assignment", "");
return;
}
frappe.query_report.set_filter_value("salary_structure_assignment", "");
if (!employee) return;
frappe.db
.get_list("Salary Structure Assignment", {
filters: { employee: employee, docstatus: 1 },
Expand All @@ -39,6 +37,8 @@ frappe.query_reports["Employee CTC Break-up"] = {
limit: 1,
})
.then(function (result) {
if (frappe.query_report.get_filter_value("salary_structure_assignment"))
return;
frappe.query_report.set_filter_value(
"salary_structure_assignment",
(result[0] && result[0].name) || "",
Expand Down Expand Up @@ -66,6 +66,8 @@ frappe.query_reports["Employee CTC Break-up"] = {
},
],
onload: async function (report) {
if (report.get_filter_value("employee")) return;

const employee = await hrms.get_current_employee();
if (!employee) return;
report.set_filter_value("employee", employee);
Expand Down
35 changes: 33 additions & 2 deletions hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self, employee, salary_structure_assignment):
self.earning_components = []
self.deduction_components = []
self.tax_components = []
self.employer_contribution_components = []
self.total_net_earnings = []
self.total_gross_earnings = []

Expand All @@ -64,6 +65,7 @@ def validate_ctc(self):

def get_data(self):
self.set_salary_component_details()
self.set_employer_contribution_details()
self.calculate_yearly_amounts_and_percent_of_ctc()
self.indent_salary_components()
self.separate_salary_components_by_type()
Expand All @@ -75,6 +77,7 @@ def get_data(self):
self.earning_components
+ self.deduction_components
+ self.tax_components
+ self.employer_contribution_components
+ self.total_net_earnings
+ self.total_gross_earnings
)
Expand Down Expand Up @@ -108,6 +111,26 @@ def set_salary_component_details(self):
)
component.update(component_details)

def set_employer_contribution_details(self):
evaluated_components = getattr(self.salary_slip, "_evaluated_components", None)
if evaluated_components is None:
evaluated_components = frappe.get_cached_doc(
"Salary Structure Assignment", self.salary_structure_assignment
).get_evaluated_components()

self.salary_components += [
{
"salary_component": component.salary_component,
"per_cycle": flt(component.default_amount),
"abbr": component.abbr,
"amount_based_on_formula": component.amount_based_on_formula,
"formula": component.formula,
"component_type": "employer_contributions",
}
for component in evaluated_components["employer_contributions"]
if not component.statistical_component
]

def calculate_yearly_amounts_and_percent_of_ctc(self):
for component in self.salary_components:
annual_amount = component.get("per_cycle", 0) * self.cycle_multiplier
Expand All @@ -130,9 +153,16 @@ def separate_salary_components_by_type(self):
self.tax_components = [
component for component in self.salary_components if component.get("is_tax_component")
]
self.employer_contribution_components = [
component
for component in self.salary_components
if component.get("component_type") == "employer_contributions"
]

def set_abbr_type_and_formula(self):
for component in self.earning_components + self.deduction_components:
for component in (
self.earning_components + self.deduction_components + self.employer_contribution_components
):
component["salary_component"] = f"{component.get("salary_component")} ({component.get("abbr")})"
component["type"] = "Formula" if component.get("amount_based_on_formula") else "Fixed"
component["formula"] = (
Expand All @@ -153,6 +183,7 @@ def set_totals_row(component_type):
"Earnings": self.earning_components,
"Deductions": self.deduction_components,
"Tax Deductions": self.tax_components,
"Employer Contributions": self.employer_contribution_components,
}.get(component_type)
totals_row = {
"salary_component": component_type,
Expand All @@ -165,7 +196,7 @@ def set_totals_row(component_type):
}
components.insert(0, totals_row)

for component_type in ("Earnings", "Deductions", "Tax Deductions"):
for component_type in ("Earnings", "Deductions", "Tax Deductions", "Employer Contributions"):
set_totals_row(component_type)

def set_net_and_gross_earning_rows(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ def test_income_tax_net_pay_calculation_at_the_start_of_payroll_period(self):
frappe.flags.posting_date = get_year_start(getdate())
ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name)
self.assertEqual(ctc_breakup.net_pay, 81396)
income_tax_row = ctc_breakup.get_data()[-3]
ctc_breakup.get_data()
income_tax_row = ctc_breakup.tax_components[-1]
self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax)
self.assertEqual(income_tax_row.get("annual"), annual_tax)

Expand All @@ -126,7 +127,8 @@ def test_income_tax_net_pay_calculation_at_the_end_of_payroll_period(self):
frappe.flags.posting_date = add_months(get_year_start(getdate()), 10)
ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name)
self.assertEqual(ctc_breakup.net_pay, 92800)
income_tax_row = ctc_breakup.get_data()[-3]
ctc_breakup.get_data()
income_tax_row = ctc_breakup.tax_components[-1]
self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax)
self.assertEqual(income_tax_row.get("annual"), annual_tax)

Expand All @@ -143,11 +145,12 @@ def test_report(self):
frappe.flags.posting_date = get_year_start(getdate())
ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name)
data = ctc_breakup.get_data()
self.assertEqual(len(data), 11)
self.assertEqual(len(data), 12)

earning_components = ctc_breakup.earning_components
deduction_components = ctc_breakup.deduction_components
tax_components = ctc_breakup.tax_components
employer_contribution_components = ctc_breakup.employer_contribution_components
net_pay_row = ctc_breakup.total_net_earnings
gross_pay_row = ctc_breakup.total_gross_earnings

Expand Down Expand Up @@ -221,6 +224,13 @@ def test_report(self):
self.assertEqual(gross_pay_row[0].get("annual"), 1116000)
self.assertEqual(gross_pay_row[0].get("percent_of_ctc"), 100)

self.assertEqual(len(employer_contribution_components), 1)
self.assertEqual(
employer_contribution_components[0].get("salary_component"), "Employer Contributions"
)
self.assertEqual(employer_contribution_components[0].get("per_cycle"), 0)
self.assertEqual(employer_contribution_components[0].get("annual"), 0)

self.assertEqual(
flt(
basic_earning_component.get("percent_of_ctc")
Expand All @@ -241,6 +251,73 @@ def test_report(self):
earning_components_totals.get("percent_of_ctc"),
)

def test_employer_contributions_section(self):
employee = make_employee("test_ctc@example.com", company="_Test Company")

for component, abbr in (("Test CTC Employer PF", "TCEPF"), ("Test CTC Employer NPS", "TCENPS")):
if frappe.db.exists("Salary Component", component):
frappe.delete_doc("Salary Component", component, force=True)
frappe.get_doc(
{
"doctype": "Salary Component",
"salary_component": component,
"salary_component_abbr": abbr,
"type": "Employer Contribution",
}
).insert()

salary_structure = make_salary_structure(
"Test CTC Breakup with Employer Contribution",
payroll_frequency="Monthly",
currency="INR",
other_details={
"employer_contributions": [
{"salary_component": "Test CTC Employer PF", "abbr": "TCEPF", "amount": 6000},
{
"salary_component": "Test CTC Employer NPS",
"abbr": "TCENPS",
"amount_based_on_formula": 1,
"formula": "BS * 0.12",
},
]
},
)
salary_structure_assignment = create_salary_structure_assignment(
employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate())
)
salary_structure_assignment.ctc = 1274400
salary_structure_assignment.save()

frappe.flags.posting_date = get_year_start(getdate())
ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name)
data = ctc_breakup.get_data()

employer_components = ctc_breakup.employer_contribution_components
self.assertEqual(data[-2], ctc_breakup.total_net_earnings[0])
self.assertEqual(data[-1], ctc_breakup.total_gross_earnings[0])
self.assertEqual(data[-2 - len(employer_components) : -2], employer_components)

totals_row = employer_components[0]
self.assertEqual(totals_row.get("salary_component"), "Employer Contributions")
self.assertTrue(totals_row.get("bold"))
self.assertEqual(totals_row.get("per_cycle"), 13200)
self.assertEqual(totals_row.get("annual"), 158400)

pf_row = employer_components[1]
self.assertEqual(pf_row.get("salary_component"), "Test CTC Employer PF (TCEPF)")
self.assertEqual(pf_row.get("type"), "Fixed")
self.assertEqual(pf_row.get("per_cycle"), 6000)
self.assertEqual(pf_row.get("annual"), 72000)
self.assertEqual(pf_row.get("percent_of_ctc"), flt(72000 * 100 / 1274400, 2))

nps_row = employer_components[2]
self.assertEqual(nps_row.get("salary_component"), "Test CTC Employer NPS (TCENPS)")
self.assertEqual(nps_row.get("type"), "Formula")
self.assertEqual(nps_row.get("formula"), "BS * 0.12")
self.assertEqual(nps_row.get("per_cycle"), 7200)
self.assertEqual(nps_row.get("annual"), 86400)
self.assertEqual(nps_row.get("percent_of_ctc"), flt(86400 * 100 / 1274400, 2))

def test_ctc_validation(self):
employee = make_employee("test_ctc@example.com", company="_Test Company")
salary_structure = make_salary_structure(
Expand Down
Loading