Skip to content

Commit

Permalink
Merge pull request #1403 from frappe/mergify/bp/version-15-hotfix/pr-…
Browse files Browse the repository at this point in the history
…1204

feat: Salary Component & Structure enhancements (backport #1204)
  • Loading branch information
ruchamahabal authored Feb 6, 2024
2 parents d202c16 + 51e41a9 commit 7948d3c
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 21 deletions.
78 changes: 75 additions & 3 deletions hrms/payroll/doctype/salary_component/salary_component.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ frappe.ui.form.on("Salary Component", {
},

refresh: function (frm) {
hrms.payroll_common.set_autocompletions_for_condition_and_formula(frm);

if (!frm.doc.__islocal) {
frm.add_custom_button(__("Salary Structure"), () => {
frm.trigger("create_salary_structure");
}, __("Create"));
frm.trigger("add_update_structure_button");
frm.add_custom_button(
__("Salary Structure"),
() => {
frm.trigger("create_salary_structure");
},
__("Create")
);
}
},

Expand Down Expand Up @@ -70,6 +77,71 @@ frappe.ui.form.on("Salary Component", {
}
},

add_update_structure_button: function (frm) {
for (const df of ["Condition", "Formula"]) {
frm.add_custom_button(
__("Sync {0}", [df]),
function () {
frappe
.call({
method: "get_structures_to_be_updated",
doc: frm.doc,
})
.then((r) => {
if (r.message.length)
frm.events.update_salary_structures(frm, df, r.message);
else
frappe.msgprint({
message: __(
"Salary Component {0} is currently not used in any Salary Structure.",
[frm.doc.name.bold()]
),
title: __("No Salary Structures"),
indicator: "orange",
});
});
},
__("Update Salary Structures")
);
}
},

update_salary_structures: function (frm, df, structures) {
let msg = __(
"{0} will be updated for the following Salary Structures: {1}.",
[
df,
frappe.utils.comma_and(
structures.map((d) =>
frappe.utils.get_form_link("Salary Structure", d, true).bold()
)
),
]
);
msg += "<br>";
msg += __("Are you sure you want to proceed?");
frappe.confirm(msg, () => {
frappe
.call({
method: "update_salary_structures",
doc: frm.doc,
args: {
structures: structures,
field: df.toLowerCase(),
value: frm.get_field(df.toLowerCase()).value,
},
})
.then((r) => {
if (!r.exc) {
frappe.show_alert({
message: __("Salary Structures updated successfully"),
indicator: "green",
});
}
});
});
},

create_salary_structure: function (frm) {
frappe.model.with_doctype("Salary Structure", () => {
const salary_structure = frappe.model.get_new_doc("Salary Structure");
Expand Down
8 changes: 5 additions & 3 deletions hrms/payroll/doctype/salary_component/salary_component.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@
{
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
"label": "Condition",
"options": "PythonExpression"
},
{
"default": "0",
Expand All @@ -198,7 +199,8 @@
"depends_on": "amount_based_on_formula",
"fieldname": "formula",
"fieldtype": "Code",
"label": "Formula"
"label": "Formula",
"options": "PythonExpression"
},
{
"depends_on": "eval:doc.amount_based_on_formula!==1",
Expand Down Expand Up @@ -266,7 +268,7 @@
"icon": "fa fa-flag",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-08-25 13:35:37.413696",
"modified": "2024-02-02 13:55:55.989527",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Component",
Expand Down
63 changes: 63 additions & 0 deletions hrms/payroll/doctype/salary_component/salary_component.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

import copy

import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists

from hrms.payroll.utils import sanitize_expression


class SalaryComponent(Document):
def before_validate(self):
self._condition, self.condition = self.condition, sanitize_expression(self.condition)
self._formula, self.formula = self.formula, sanitize_expression(self.formula)

def validate(self):
self.validate_abbr()
self.validate_accounts()

def on_update(self):
# set old values (allowing multiline strings for better readability in the doctype form)
if self._condition != self.condition:
self.db_set("condition", self._condition)
if self._formula != self.formula:
self.db_set("formula", self._formula)

def clear_cache(self):
from hrms.payroll.doctype.salary_slip.salary_slip import (
Expand All @@ -32,3 +49,49 @@ def validate_abbr(self):
separator="_",
filters={"name": ["!=", self.name]},
)

def validate_accounts(self):
if not (self.statistical_component or (self.accounts and all(d.account for d in self.accounts))):
frappe.msgprint(
title=_("Warning"),
msg=_("Accounts not set for Salary Component {0}").format(self.name),
indicator="orange",
)

@frappe.whitelist()
def get_structures_to_be_updated(self):
SalaryStructure = frappe.qb.DocType("Salary Structure")
SalaryDetail = frappe.qb.DocType("Salary Detail")
return (
frappe.qb.from_(SalaryStructure)
.inner_join(SalaryDetail)
.on(SalaryStructure.name == SalaryDetail.parent)
.select(SalaryStructure.name)
.where((SalaryDetail.salary_component == self.name) & (SalaryStructure.docstatus != 2))
.run(pluck=True)
)

@frappe.whitelist()
def update_salary_structures(self, field, value, structures=None):
if not structures:
structures = self.get_structures_to_be_updated()

for structure in structures:
salary_structure = frappe.get_doc("Salary Structure", structure)
# this is only used for versioning and we do not want
# to make separate db calls by using load_doc_before_save
# which proves to be expensive while doing bulk replace
salary_structure._doc_before_save = copy.deepcopy(salary_structure)

salary_detail_row = next(
(d for d in salary_structure.get(f"{self.type.lower()}s") if d.salary_component == self.name),
None,
)
salary_detail_row.set(field, value)
salary_structure.db_update_all()
salary_structure.flags.updater_reference = {
"doctype": self.doctype,
"docname": self.name,
"label": _("via Salary Component sync"),
}
salary_structure.save_version()
47 changes: 46 additions & 1 deletion hrms/payroll/doctype/salary_component/test_salary_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,54 @@
import frappe
from frappe.tests.utils import FrappeTestCase

from hrms.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure


class TestSalaryComponent(FrappeTestCase):
pass
def test_update_salary_structures(self):
salary_component = create_salary_component("Special Allowance")
salary_component.condition = "H < 10000"
salary_component.formula = "BS*.5"
salary_component.save()

salary_structure1 = make_salary_structure("Salary Structure 1", "Monthly")
salary_structure2 = make_salary_structure("Salary Structure 2", "Monthly")
salary_structure3 = make_salary_structure("Salary Structure 3", "Monthly")
salary_structure3.cancel() # Details should not update for cancelled Salary Structures

ss1_detail = [
d for d in salary_structure1.earnings if d.salary_component == "Special Allowance"
][0]
self.assertEqual(ss1_detail.condition, "H < 10000")
self.assertEqual(ss1_detail.formula, "BS*.5")

ss2_detail = [
d for d in salary_structure2.earnings if d.salary_component == "Special Allowance"
][0]
self.assertEqual(ss2_detail.condition, "H < 10000")
self.assertEqual(ss2_detail.formula, "BS*.5")

ss3_detail = [
d for d in salary_structure3.earnings if d.salary_component == "Special Allowance"
][0]
self.assertEqual(ss3_detail.condition, "H < 10000")
self.assertEqual(ss3_detail.formula, "BS*.5")

salary_component.update_salary_structures("condition", "H < 8000")
ss1_detail.reload()
self.assertEqual(ss1_detail.condition, "H < 8000")
ss2_detail.reload()
self.assertEqual(ss2_detail.condition, "H < 8000")
ss3_detail.reload()
self.assertEqual(ss3_detail.condition, "H < 10000")

salary_component.update_salary_structures("formula", "BS*.3")
ss1_detail.reload()
self.assertEqual(ss1_detail.formula, "BS*.3")
ss2_detail.reload()
self.assertEqual(ss2_detail.formula, "BS*.3")
ss3_detail.reload()
self.assertEqual(ss3_detail.formula, "BS*.5")


def create_salary_component(component_name, **args):
Expand Down
8 changes: 5 additions & 3 deletions hrms/payroll/doctype/salary_detail/salary_detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
"label": "Condition",
"options": "PythonExpression"
},
{
"default": "0",
Expand All @@ -147,7 +148,8 @@
"fieldname": "formula",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Formula"
"label": "Formula",
"options": "PythonExpression"
},
{
"depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'",
Expand Down Expand Up @@ -255,7 +257,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-12-12 13:52:30.726505",
"modified": "2024-02-02 16:10:45.570565",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
Expand Down
5 changes: 5 additions & 0 deletions hrms/payroll/doctype/salary_structure/salary_structure.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ cur_frm.cscript.validate = function(doc, cdt, cdn) {


frappe.ui.form.on('Salary Detail', {
form_render: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
hrms.payroll_common.set_autocompletions_for_condition_and_formula(frm, row);
},

amount: function(frm) {
calculate_totals(frm.doc);
},
Expand Down
5 changes: 3 additions & 2 deletions hrms/payroll/doctype/salary_structure/salary_structure.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-12-12 14:11:22.774017",
"modified": "2024-02-06 13:06:59.579135",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure",
Expand Down Expand Up @@ -281,5 +281,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"states": [],
"track_changes": 1
}
32 changes: 23 additions & 9 deletions hrms/payroll/doctype/salary_structure/salary_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@

import erpnext

from hrms.payroll.utils import sanitize_expression


class SalaryStructure(Document):
def before_validate(self):
self.sanitize_condition_and_formula_fields()

def validate(self):
self.set_missing_values()
self.validate_amount()
self.strip_condition_and_formula_fields()
self.validate_max_benefits_with_flexi()
self.validate_component_based_on_tax_slab()
self.validate_payment_days_based_dependent_component()
self.validate_timesheet_component()
self.validate_formula_setup()

def on_update(self):
self.reset_condition_and_formula_fields()

def validate_formula_setup(self):
for table in ["earnings", "deductions"]:
for row in self.get(table):
Expand Down Expand Up @@ -121,15 +128,22 @@ def validate_timesheet_component(self):
)
break

def strip_condition_and_formula_fields(self):
# remove whitespaces from condition and formula fields
for row in self.earnings:
row.condition = row.condition.strip() if row.condition else ""
row.formula = row.formula.strip() if row.formula else ""
def sanitize_condition_and_formula_fields(self):
for table in ("earnings", "deductions"):
for row in self.get(table):
row.condition = row.condition.strip() if row.condition else ""
row.formula = row.formula.strip() if row.formula else ""
row._condition, row.condition = row.condition, sanitize_expression(row.condition)
row._formula, row.formula = row.formula, sanitize_expression(row.formula)

def reset_condition_and_formula_fields(self):
# set old values (allowing multiline strings for better readability in the doctype form)
for table in ("earnings", "deductions"):
for row in self.get(table):
row.condition = row._condition
row.formula = row._formula

for row in self.deductions:
row.condition = row.condition.strip() if row.condition else ""
row.formula = row.formula.strip() if row.formula else ""
self.db_update_all()

def validate_max_benefits_with_flexi(self):
have_a_flexi = False
Expand Down
1 change: 1 addition & 0 deletions hrms/public/js/hrms.bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import "./templates/feedback_summary.html";
import "./templates/feedback_history.html";
import "./templates/rating.html";
import "./utils";
import "./payroll_common";
Loading

0 comments on commit 7948d3c

Please sign in to comment.