diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index 569db0adc3..e2819a6bc5 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import ast import unicodedata from datetime import date @@ -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( @@ -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( @@ -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""" @@ -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"} @@ -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)]