Skip to content

Commit

Permalink
fix(Preview Salary Slip): computation of earnings whose formula is de…
Browse files Browse the repository at this point in the history
…pendent on deductions
  • Loading branch information
krantheman committed Aug 16, 2024
1 parent da42f67 commit 915915f
Showing 1 changed file with 70 additions and 45 deletions.
115 changes: 70 additions & 45 deletions hrms/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt


import ast
import unicodedata
from datetime import date

Expand Down Expand Up @@ -765,9 +766,6 @@ def set_salary_structure_assignment(self):
)

def calculate_net_pay(self):
if self.salary_structure:
self.calculate_component_amounts("earnings")

# get remaining numbers of sub-period (period for which one salary is processed)
if self.payroll_period:
self.remaining_sub_periods = get_period_factor(
Expand All @@ -780,20 +778,40 @@ def calculate_net_pay(self):
relieving_date=self.relieving_date,
)[1]

if self.salary_structure:
self.calculate_component_amounts("earnings")
self.calculate_component_amounts("deductions")

deductions_abbr = [d.abbr for d in self.deductions]
for d in self._salary_structure_doc.earnings:
if not d.amount_based_on_formula:
continue
for var in get_variables_from_formula(d.formula):
if var in deductions_abbr:
self.add_structure_component(d, "earnings")
self.update_dependent_components_recursively("deductions", d.abbr)

self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(
flt(self.gross_pay) * flt(self.exchange_rate), self.precision("base_gross_pay")
)

if self.salary_structure:
self.calculate_component_amounts("deductions")

set_loan_repayment(self)

self.set_precision_for_component_amounts()
self.set_net_pay()
self.compute_income_tax_breakup()

def update_dependent_components_recursively(self, component_type: str, updated_var: str) -> None:
other_component_type = "deductions" if component_type == "earnings" else "earnings"

for d in self._salary_structure_doc.get(component_type):
if not d.amount_based_on_formula:
continue
for var in get_variables_from_formula(d.formula):
if var == updated_var:
self.add_structure_component(d, component_type)
self.update_dependent_components_recursively(other_component_type, d.abbr)

def set_net_pay(self):
self.total_deduction = self.get_component_totals("deductions")
self.base_total_deduction = flt(
Expand Down Expand Up @@ -1082,49 +1100,54 @@ def calculate_component_amounts(self, component_type):

def add_structure_components(self, component_type):
self.data, self.default_data = self.get_data_for_eval()
timesheet_component = self._salary_structure_doc.salary_component

for struct_row in self._salary_structure_doc.get(component_type):
if self.salary_slip_based_on_timesheet and struct_row.salary_component == timesheet_component:
continue
self.add_structure_component(struct_row, component_type)

amount = self.eval_condition_and_formula(struct_row, self.data)
if struct_row.statistical_component:
# update statitical component amount in reference data based on payment days
# since row for statistical component is not added to salary slip

self.default_data[struct_row.abbr] = flt(amount)
if struct_row.depends_on_payment_days:
payment_days_amount = (
flt(amount) * flt(self.payment_days) / cint(self.total_working_days)
if self.total_working_days
else 0
)
self.data[struct_row.abbr] = flt(payment_days_amount, struct_row.precision("amount"))
def add_structure_component(self, struct_row, component_type):
if (
self.salary_slip_based_on_timesheet
and struct_row.salary_component == self._salary_structure_doc.salary_component
):
return

else:
# default behavior, the system does not add if component amount is zero
# if remove_if_zero_valued is unchecked, then ask system to add component row
remove_if_zero_valued = frappe.get_cached_value(
"Salary Component", struct_row.salary_component, "remove_if_zero_valued"
amount = self.eval_condition_and_formula(struct_row, self.data)
if struct_row.statistical_component:
# update statitical component amount in reference data based on payment days
# since row for statistical component is not added to salary slip

self.default_data[struct_row.abbr] = flt(amount)
if struct_row.depends_on_payment_days:
payment_days_amount = (
flt(amount) * flt(self.payment_days) / cint(self.total_working_days)
if self.total_working_days
else 0
)
self.data[struct_row.abbr] = flt(payment_days_amount, struct_row.precision("amount"))

default_amount = 0
else:
# default behavior, the system does not add if component amount is zero
# if remove_if_zero_valued is unchecked, then ask system to add component row
remove_if_zero_valued = frappe.get_cached_value(
"Salary Component", struct_row.salary_component, "remove_if_zero_valued"
)

if (
amount
or (struct_row.amount_based_on_formula and amount is not None)
or (not remove_if_zero_valued and amount is not None and not self.data[struct_row.abbr])
):
default_amount = self.eval_condition_and_formula(struct_row, self.default_data)
self.update_component_row(
struct_row,
amount,
component_type,
data=self.data,
default_amount=default_amount,
remove_if_zero_valued=remove_if_zero_valued,
)
default_amount = 0

if (
amount
or (struct_row.amount_based_on_formula and amount is not None)
or (not remove_if_zero_valued and amount is not None and not self.data[struct_row.abbr])
):
default_amount = self.eval_condition_and_formula(struct_row, self.default_data)
self.update_component_row(
struct_row,
amount,
component_type,
data=self.data,
default_amount=default_amount,
remove_if_zero_valued=remove_if_zero_valued,
)

def get_data_for_eval(self):
"""Returns data for evaluating formula"""
Expand Down Expand Up @@ -2274,8 +2297,6 @@ def _safe_eval(code: str, eval_globals: dict | None = None, eval_locals: dict |


def _check_attributes(code: str) -> None:
import ast

from frappe.utils.safe_exec import UNSAFE_ATTRIBUTES

unsafe_attrs = set(UNSAFE_ATTRIBUTES).union(["__"]) - {"format"}
Expand Down Expand Up @@ -2314,3 +2335,7 @@ def email_salary_slips(names) -> None:
for name in names:
salary_slip = frappe.get_doc("Salary Slip", name)
salary_slip.email_salary_slip()


def get_variables_from_formula(formula: str) -> list[str]:
return [node.id for node in ast.walk(ast.parse(formula)) if isinstance(node, ast.Name)]

0 comments on commit 915915f

Please sign in to comment.