Salary Structure Refactor, Formula on Salary Component Master (#13967)

* Salary structure refactor

* Salary Structure Assignment - filters applied

* Formula on Salary Component Master

* Salary Structure - filter updated, Salary Component - fields re-arranged

* Payroll Entry - get_employee_list fix

* Salary Structure Assignment - Validate Duplicate Assignment

* Salary Structure Assignment - filters for salary structure
This commit is contained in:
Jamsheer 2018-05-11 21:05:24 +05:30 committed by Nabin Hait
parent c53e35368d
commit 46b23f8e6e
8 changed files with 430 additions and 44 deletions

View File

@ -40,15 +40,16 @@ class PayrollEntry(Document):
{"company": self.company, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
if sal_struct:
cond += "and t2.parent IN %(sal_struct)s "
cond += "and t2.salary_structure IN %(sal_struct)s "
emp_list = frappe.db.sql("""
select
t1.name as employee, t1.employee_name, t1.department, t1.designation
from
`tabEmployee` t1, `tabSalary Structure Employee` t2
`tabEmployee` t1, `tabSalary Structure Assignment` t2
where
t1.docstatus!=2
and t1.name = t2.employee
and t2.docstatus = 1
%s """% cond, {"sal_struct": sal_struct}, as_dict=True)
return emp_list

View File

@ -610,6 +610,320 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "condition_and_formula",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Condition and Formula",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "condition",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Condition",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ",
"fieldname": "statistical_component",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Statistical Component",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "depends_on_lwp",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Depends on Leave Without Pay",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "do_not_include_in_total",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Do not include in total",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "amount_based_on_formula",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount based on formula",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.amount_based_on_formula!==0",
"fieldname": "formula",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Formula",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.amount_based_on_formula!==1",
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_28",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "help",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Help",
"length": 0,
"no_copy": 0,
"options": "<h3>Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base &lt; 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS &gt; 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
@ -623,7 +937,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-27 13:23:34.503504",
"modified": "2018-05-09 17:35:11.073733",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Component",

View File

@ -104,8 +104,8 @@ class SalarySlip(TransactionBase):
'''Returns data for evaluating formula'''
data = frappe._dict()
data.update(frappe.get_doc("Salary Structure Employee",
{"employee": self.employee, "parent": self.salary_structure}).as_dict())
data.update(frappe.get_doc("Salary Structure Assignment",
{"employee": self.employee, "salary_structure": self.salary_structure}).as_dict())
data.update(frappe.get_doc("Employee", self.employee).as_dict())
data.update(self.as_dict())
@ -166,10 +166,10 @@ class SalarySlip(TransactionBase):
if self.payroll_frequency:
cond = """and payroll_frequency = '%(payroll_frequency)s'""" % {"payroll_frequency": self.payroll_frequency}
st_name = frappe.db.sql("""select parent from `tabSalary Structure Employee`
st_name = frappe.db.sql("""select salary_structure from `tabSalary Structure Assignment`
where employee=%s and (from_date <= %s or from_date <= %s)
and (to_date is null or to_date >= %s or to_date >= %s)
and parent in (select name from `tabSalary Structure`
and salary_structure in (select name from `tabSalary Structure`
where is_active = 'Yes'%s)
"""% ('%s', '%s', '%s','%s','%s', cond),(self.employee, self.start_date, joining_date, self.end_date, relieving_date))
@ -327,7 +327,7 @@ class SalarySlip(TransactionBase):
def sum_components(self, component_type, total_field):
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
if not relieving_date:
relieving_date = getdate(self.end_date)
@ -463,4 +463,4 @@ def unlink_ref_doc_from_salary_slip(ref_no):
if linked_ss:
for ss in linked_ss:
ss_doc = frappe.get_doc("Salary Slip", ss)
frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "")
frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "")

View File

@ -7,6 +7,7 @@ import frappe
import erpnext
import calendar
from erpnext.accounts.utils import get_fiscal_year
from frappe.utils.make_random import get_random
from frappe.utils import getdate, nowdate, add_days, add_months, flt
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.hr.doctype.payroll_entry.test_payroll_entry import get_salary_component_account
@ -272,33 +273,31 @@ def make_salary_structure(sal_struct, payroll_frequency, employee):
frappe.get_doc({
"doctype": "Salary Structure",
"name": sal_struct,
"company": erpnext.get_default_company(),
"employees": get_employee_details(employee),
"company": "_Test Company",
"earnings": get_earnings_component(),
"deductions": get_deductions_component(),
"payroll_frequency": payroll_frequency,
"payment_account": frappe.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name")
"payment_account": get_random("Account")
}).insert()
elif not frappe.db.get_value("Salary Structure Employee",{'parent':sal_struct, 'employee':employee},'name'):
create_salary_structure_assignment(employee, sal_struct)
elif not frappe.db.get_value("Salary Structure Assignment",{'salary_structure':sal_struct, 'employee':employee},'name'):
sal_struct = frappe.get_doc("Salary Structure", sal_struct)
sal_struct.append("employees", {"employee": employee,
"employee_name": employee,
"base": 32000,
"variable": 3200,
"from_date": add_months(nowdate(),-1)
})
sal_struct.save()
create_salary_structure_assignment(employee, sal_struct)
sal_struct = sal_struct.name
return sal_struct
def get_employee_details(employee):
return [{"employee": employee,
"base": 50000,
"variable": 5000,
"from_date": add_months(nowdate(),-1)
}
]
def create_salary_structure_assignment(employee, salary_structure):
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee
salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = add_months(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.company = erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
return salary_structure_assignment
def get_earnings_component():
return [
@ -353,4 +352,4 @@ def get_deductions_component():
"formula": 'base*.1',
"idx": 3
}
]
]

View File

@ -165,5 +165,46 @@ frappe.ui.form.on('Salary Detail', {
deductions_remove: function(frm) {
calculate_totals(frm.doc);
},
salary_component: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if(child.salary_component){
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Salary Component",
name: child.salary_component
},
callback: function(data) {
if(data.message){
var result = data.message;
frappe.model.set_value(cdt, cdn, 'condition',result.condition);
frappe.model.set_value(cdt, cdn, 'amount_based_on_formula',result.amount_based_on_formula);
if(result.amount_based_on_formula == 1){
frappe.model.set_value(cdt, cdn, 'formula',result.formula);
}
else{
frappe.model.set_value(cdt, cdn, 'amount',result.amount);
}
frappe.model.set_value(cdt, cdn, 'statistical_component',result.statistical_component);
frappe.model.set_value(cdt, cdn, 'depends_on_lwp',result.depends_on_lwp);
frappe.model.set_value(cdt, cdn, 'do_not_include_in_total',result.do_not_include_in_total);
refresh_field("earnings");
refresh_field("deductions");
}
}
});
}
},
amount_based_on_formula: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if(child.amount_based_on_formula == 1){
frappe.model.set_value(cdt, cdn, 'amount', null);
}
else{
frappe.model.set_value(cdt, cdn, 'formula', null);
}
}
})

View File

@ -11,9 +11,36 @@ frappe.ui.form.on('Salary Structure Assignment', {
}
}
});
frm.set_query("salary_structure", function() {
return {
filters: {
company: frm.doc.company,
is_active: "Yes",
docstatus: 1
}
}
});
},
refresh: function(frm) {
employee: function(frm) {
if(frm.doc.employee){
frappe.call({
method: "frappe.client.get_value",
args:{
doctype: "Employee",
fieldname: "company",
filters:{
name: frm.doc.employee
}
},
callback: function(data) {
if(data.message){
frm.set_value("company", data.message.company);
}
}
});
}
else{
frm.set_value("company", null);
}
}
});

View File

@ -11,6 +11,7 @@ from frappe.model.document import Document
class SalaryStructureAssignment(Document):
def validate(self):
self.validate_dates()
self.validate_duplicate_assignments()
def validate_dates(self):
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
@ -33,10 +34,14 @@ class SalaryStructureAssignment(Document):
.format(self.to_date, relieving_date))
def validate_duplicate_assignments(self):
if not self.name:
# hack! if name is null, it could cause problems with !=
self.name = "New "+self.doctype
assignment = frappe.db.sql("""
select name from `tabSalary Structure Assignment`
where employee=%(employee)s
and name != %(salary_struct)s
and name != %(name)s
and docstatus != 2
and (
(%(from_date)s between from_date and ifnull(to_date, '2199-12-31'))
or (%(to_date)s between from_date and ifnull(to_date, '2199-12-31'))
@ -45,8 +50,8 @@ class SalaryStructureAssignment(Document):
'employee': self.employee,
'from_date': self.from_date,
'to_date': (self.to_date or '2199-12-31'),
'salary_struct': self.salary_struct
'name': self.name
})
if assignment:
frappe.throw(_("Active Salary Structure Assignment {0} found for employee {1} for the given dates").format(assignment[0][0], self.employee))
frappe.throw(_("Active Salary Structure Assignment {0} found for employee {1} for the given dates").format(assignment[0][0], self.employee))

View File

@ -118,30 +118,21 @@ class TestTimesheet(unittest.TestCase):
def make_salary_structure(employee):
name = frappe.db.get_value('Salary Structure Employee', {'employee': employee}, 'parent')
name = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'salary_structure')
if name:
salary_structure = frappe.get_doc('Salary Structure', name)
else:
salary_structure = frappe.new_doc("Salary Structure")
salary_structure.name = "Timesheet Salary Structure Test"
salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.from_date = add_days(nowdate(), -30)
salary_structure.salary_component = "Basic"
salary_structure.hour_rate = 50.0
salary_structure.company = "_Test Company"
salary_structure.payment_account = get_random("Account")
salary_structure.set('employees', [])
salary_structure.set('earnings', [])
salary_structure.set('deductions', [])
es = salary_structure.append('employees', {
"employee": employee,
"base": 1200,
"from_date": add_months(nowdate(),-1)
})
es = salary_structure.append('earnings', {
"salary_component": "_Test Allowance",
"amount": 100
@ -154,6 +145,14 @@ def make_salary_structure(employee):
salary_structure.save(ignore_permissions=True)
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee
salary_structure_assignment.base = 1200
salary_structure_assignment.from_date = add_months(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure.name
salary_structure_assignment.company = "_Test Company"
salary_structure_assignment.save(ignore_permissions=True)
return salary_structure
def make_timesheet(employee, simulate=False, billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):