From 46b23f8e6e8d2b7e19f88912c742c6a33b3f7011 Mon Sep 17 00:00:00 2001 From: Jamsheer Date: Fri, 11 May 2018 21:05:24 +0530 Subject: [PATCH] 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 --- .../hr/doctype/payroll_entry/payroll_entry.py | 5 +- .../salary_component/salary_component.json | 316 +++++++++++++++++- erpnext/hr/doctype/salary_slip/salary_slip.py | 12 +- .../doctype/salary_slip/test_salary_slip.py | 37 +- .../salary_structure/salary_structure.js | 41 +++ .../salary_structure_assignment.js | 33 +- .../salary_structure_assignment.py | 11 +- .../doctype/timesheet/test_timesheet.py | 19 +- 8 files changed, 430 insertions(+), 44 deletions(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 1025bc7dae..e1b841f9b9 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -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 diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 27b4bef036..5f875a9948 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -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": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
", + "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", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 75eb73b532..99de580a3f 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -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", "") \ No newline at end of file + frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "") diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index cced29d7d9..ae58298bff 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -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 } - ] \ No newline at end of file + ] diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/hr/doctype/salary_structure/salary_structure.js index 3de01cd303..6f5c923a35 100755 --- a/erpnext/hr/doctype/salary_structure/salary_structure.js +++ b/erpnext/hr/doctype/salary_structure/salary_structure.js @@ -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); + } } }) diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js index e7c6598653..af4ca3a3b5 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -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); + } } }); diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py index c9269d7c92..ee2920be2c 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -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)) \ No newline at end of file + frappe.throw(_("Active Salary Structure Assignment {0} found for employee {1} for the given dates").format(assignment[0][0], self.employee)) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 2458db0cba..d2017c5712 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -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):