Introduce the ability to specify in a Salary Structure that a component is statistical. This allows components to be used in calculations without being added/deducted from earnings deductions.

This commit is contained in:
ckosiegbu 2017-04-13 00:00:37 +01:00
parent 4782e8b751
commit 64f29f819a
3 changed files with 519 additions and 426 deletions

View File

@ -1,5 +1,6 @@
{ {
"allow_copy": 0, "allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0, "allow_import": 0,
"allow_rename": 0, "allow_rename": 0,
"beta": 0, "beta": 0,
@ -21,7 +22,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Component", "label": "Component",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -31,6 +34,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 0,
@ -49,7 +53,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Abbr", "label": "Abbr",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -59,6 +65,65 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"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,
"unique": 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": 1,
"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, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -76,7 +141,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@ -84,6 +151,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -102,7 +170,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Condition", "label": "Condition",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -111,6 +181,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -130,7 +201,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Amount based on formula", "label": "Amount based on formula",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -140,6 +213,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -160,7 +234,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Formula", "label": "Formula",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -169,6 +245,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -187,7 +264,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Amount", "label": "Amount",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -197,6 +276,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -215,7 +295,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Depends on Leave Without Pay", "label": "Depends on Leave Without Pay",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -224,6 +306,7 @@
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -242,7 +325,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Amount", "label": "Default Amount",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -252,6 +337,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -270,7 +356,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@ -278,6 +366,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -296,7 +385,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Condition and Formula Help", "label": "Condition and Formula Help",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -306,6 +397,7 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -313,18 +405,18 @@
"unique": 0 "unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0, "hide_heading": 0,
"hide_toolbar": 0, "hide_toolbar": 0,
"idx": 0, "idx": 0,
"image_view": 0, "image_view": 0,
"in_create": 0, "in_create": 0,
"in_dialog": 0,
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "max_attachments": 0,
"modified": "2016-09-20 05:29:26.373992", "modified": "2017-04-12 22:47:33.980646",
"modified_by": "Administrator", "modified_by": "chude.osiegbu@manqala.com",
"module": "HR", "module": "HR",
"name": "Salary Detail", "name": "Salary Detail",
"name_case": "", "name_case": "",
@ -333,7 +425,9 @@
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "read_only": 0,
"read_only_onload": 0, "read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0,
"track_seen": 0 "track_seen": 0
} }

View File

@ -39,7 +39,7 @@ frappe.ui.form.on("Salary Slip", {
refresh: function(frm) { refresh: function(frm) {
frm.trigger("toggle_fields") frm.trigger("toggle_fields")
frm.trigger("toggle_reqd_fields") frm.trigger("toggle_reqd_fields")
salary_detail_fields = ['formula', 'abbr'] salary_detail_fields = ['formula', 'abbr', 'statistical_component']
cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false); cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false);
cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false); cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false);
}, },
@ -129,16 +129,15 @@ var calculate_earning_total = function(doc, dt, dn, reset_amount) {
var tbl = doc.earnings || []; var tbl = doc.earnings || [];
var total_earn = 0; var total_earn = 0;
for(var i = 0; i < tbl.length; i++){ for(var i = 0; i < tbl.length; i++){
if(cint(tbl[i].depends_on_lwp) == 1) { if(cint(tbl[i].depends_on_lwp) == 1) {
tbl[i].amount = Math.round(tbl[i].default_amount)*(flt(doc.payment_days) / tbl[i].amount = Math.round(tbl[i].default_amount)*(flt(doc.payment_days) /
cint(doc.total_working_days)*100)/100; cint(doc.total_working_days)*100)/100;
refresh_field('amount', tbl[i].name, 'earnings'); refresh_field('amount', tbl[i].name, 'earnings');
} else if(reset_amount) { } else if(reset_amount) {
tbl[i].amount = tbl[i].default_amount; tbl[i].amount = tbl[i].default_amount;
refresh_field('amount', tbl[i].name, 'earnings'); refresh_field('amount', tbl[i].name, 'earnings');
} }
total_earn += flt(tbl[i].amount); total_earn += flt(tbl[i].amount);
} }
doc.gross_pay = total_earn; doc.gross_pay = total_earn;
refresh_many(['amount','gross_pay']); refresh_many(['amount','gross_pay']);
@ -150,14 +149,14 @@ var calculate_ded_total = function(doc, dt, dn, reset_amount) {
var tbl = doc.deductions || []; var tbl = doc.deductions || [];
var total_ded = 0; var total_ded = 0;
for(var i = 0; i < tbl.length; i++){ for(var i = 0; i < tbl.length; i++){
if(cint(tbl[i].depends_on_lwp) == 1) { if(cint(tbl[i].depends_on_lwp) == 1) {
tbl[i].amount = Math.round(tbl[i].default_amount)*(flt(doc.payment_days)/cint(doc.total_working_days)*100)/100; tbl[i].amount = Math.round(tbl[i].default_amount)*(flt(doc.payment_days)/cint(doc.total_working_days)*100)/100;
refresh_field('amount', tbl[i].name, 'deductions'); refresh_field('amount', tbl[i].name, 'deductions');
} else if(reset_amount) { } else if(reset_amount) {
tbl[i].amount = tbl[i].default_amount; tbl[i].amount = tbl[i].default_amount;
refresh_field('amount', tbl[i].name, 'deductions'); refresh_field('amount', tbl[i].name, 'deductions');
} }
total_ded += flt(tbl[i].amount); total_ded += flt(tbl[i].amount);
} }
doc.total_deduction = total_ded; doc.total_deduction = total_ded;
refresh_field('total_deduction'); refresh_field('total_deduction');

View File

@ -13,409 +13,409 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
def autoname(self): def autoname(self):
self.name = make_autoname('Sal Slip/' +self.employee + '/.#####') self.name = make_autoname('Sal Slip/' +self.employee + '/.#####')
def validate(self): def validate(self):
self.status = self.get_status() self.status = self.get_status()
self.validate_dates() self.validate_dates()
self.check_existing() self.check_existing()
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
self.get_date_details() self.get_date_details()
if not (len(self.get("earnings")) or len(self.get("deductions"))): if not (len(self.get("earnings")) or len(self.get("deductions"))):
# get details from salary structure # get details from salary structure
self.get_emp_and_leave_details() self.get_emp_and_leave_details()
else: else:
self.get_leave_details(lwp = self.leave_without_pay) self.get_leave_details(lwp = self.leave_without_pay)
# if self.salary_slip_based_on_timesheet or not self.net_pay: # if self.salary_slip_based_on_timesheet or not self.net_pay:
self.calculate_net_pay() self.calculate_net_pay()
company_currency = erpnext.get_company_currency(self.company) company_currency = erpnext.get_company_currency(self.company)
self.total_in_words = money_in_words(self.rounded_total, company_currency) self.total_in_words = money_in_words(self.rounded_total, company_currency)
if frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet"): if frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet") max_working_hours = frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet")
if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)):
frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). frappe.msgprint(_("Total working hours should not be greater than max working hours {0}").
format(max_working_hours), alert=True) format(max_working_hours), alert=True)
def validate_dates(self): def validate_dates(self):
if date_diff(self.end_date, self.start_date) < 0: if date_diff(self.end_date, self.start_date) < 0:
frappe.throw(_("To date cannot be before From date")) frappe.throw(_("To date cannot be before From date"))
def calculate_component_amounts(self): def calculate_component_amounts(self):
if not getattr(self, '_salary_structure_doc', None): if not getattr(self, '_salary_structure_doc', None):
self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure) self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure)
data = self.get_data_for_eval() data = self.get_data_for_eval()
for key in ('earnings', 'deductions'): for key in ('earnings', 'deductions'):
for struct_row in self._salary_structure_doc.get(key): for struct_row in self._salary_structure_doc.get(key):
amount = self.eval_condition_and_formula(struct_row, data) amount = self.eval_condition_and_formula(struct_row, data)
if amount: if amount and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, key) self.update_component_row(struct_row, amount, key)
def update_component_row(self, struct_row, amount, key): def update_component_row(self, struct_row, amount, key):
component_row = None component_row = None
for d in self.get(key): for d in self.get(key):
if d.salary_component == struct_row.salary_component: if d.salary_component == struct_row.salary_component:
component_row = d component_row = d
if not component_row: if not component_row:
self.append(key, { self.append(key, {
'amount': amount, 'amount': amount,
'default_amount': amount, 'default_amount': amount,
'depends_on_lwp' : struct_row.depends_on_lwp, 'depends_on_lwp' : struct_row.depends_on_lwp,
'salary_component' : struct_row.salary_component 'salary_component' : struct_row.salary_component
}) })
else: else:
component_row.amount = amount component_row.amount = amount
def eval_condition_and_formula(self, d, data): def eval_condition_and_formula(self, d, data):
try: try:
if d.condition: if d.condition:
if not frappe.safe_eval(d.condition, None, data): if not frappe.safe_eval(d.condition, None, data):
return None return None
amount = d.amount amount = d.amount
if d.amount_based_on_formula: if d.amount_based_on_formula:
if d.formula: if d.formula:
amount = frappe.safe_eval(d.formula, None, data) amount = frappe.safe_eval(d.formula, None, data)
if amount: if amount:
data[d.abbr] = amount data[d.abbr] = amount
return amount return amount
except NameError as err: except NameError as err:
frappe.throw(_("Name error: {0}".format(err))) frappe.throw(_("Name error: {0}".format(err)))
except SyntaxError as err: except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}".format(err))) frappe.throw(_("Syntax error in formula or condition: {0}".format(err)))
except Exception, e: except Exception, e:
frappe.throw(_("Error in formula or condition: {0}".format(e))) frappe.throw(_("Error in formula or condition: {0}".format(e)))
raise raise
def get_data_for_eval(self): def get_data_for_eval(self):
'''Returns data for evaluating formula''' '''Returns data for evaluating formula'''
data = frappe._dict() data = frappe._dict()
data.update(frappe.get_doc("Salary Structure Employee", {"employee": self.employee}).as_dict()) data.update(frappe.get_doc("Salary Structure Employee", {"employee": self.employee}).as_dict())
data.update(frappe.get_doc("Employee", self.employee).as_dict()) data.update(frappe.get_doc("Employee", self.employee).as_dict())
data.update(self.as_dict()) data.update(self.as_dict())
# set values for components # set values for components
salary_components = frappe.get_all("Salary Component", fields=["salary_component_abbr"]) salary_components = frappe.get_all("Salary Component", fields=["salary_component_abbr"])
for sc in salary_components: for sc in salary_components:
data.setdefault(sc.salary_component_abbr, 0) data.setdefault(sc.salary_component_abbr, 0)
for key in ('earnings', 'deductions'): for key in ('earnings', 'deductions'):
for d in self.get(key): for d in self.get(key):
data[d.abbr] = d.amount data[d.abbr] = d.amount
return data return data
def get_emp_and_leave_details(self): def get_emp_and_leave_details(self):
'''First time, load all the components from salary structure''' '''First time, load all the components from salary structure'''
if self.employee: if self.employee:
self.set("earnings", []) self.set("earnings", [])
self.set("deductions", []) self.set("deductions", [])
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
self.get_date_details() self.get_date_details()
self.validate_dates() self.validate_dates()
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
self.get_leave_details(joining_date, relieving_date) self.get_leave_details(joining_date, relieving_date)
struct = self.check_sal_struct(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date)
if struct: if struct:
self._salary_structure_doc = frappe.get_doc('Salary Structure', struct) self._salary_structure_doc = frappe.get_doc('Salary Structure', struct)
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet() self.set_time_sheet()
self.pull_sal_struct() self.pull_sal_struct()
def set_time_sheet(self): def set_time_sheet(self):
if self.salary_slip_based_on_timesheet: if self.salary_slip_based_on_timesheet:
self.set("timesheets", []) self.set("timesheets", [])
timesheets = frappe.db.sql(""" select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or timesheets = frappe.db.sql(""" select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or
status = 'Billed')""", {'employee': self.employee, 'start_date': self.start_date, 'end_date': self.end_date}, as_dict=1) status = 'Billed')""", {'employee': self.employee, 'start_date': self.start_date, 'end_date': self.end_date}, as_dict=1)
for data in timesheets: for data in timesheets:
self.append('timesheets', { self.append('timesheets', {
'time_sheet': data.name, 'time_sheet': data.name,
'working_hours': data.total_hours 'working_hours': data.total_hours
}) })
def get_date_details(self): def get_date_details(self):
date_details = get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date) date_details = get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date)
self.start_date = date_details.start_date self.start_date = date_details.start_date
self.end_date = date_details.end_date self.end_date = date_details.end_date
def check_sal_struct(self, joining_date, relieving_date): def check_sal_struct(self, joining_date, relieving_date):
cond = '' cond = ''
if self.payroll_frequency: if self.payroll_frequency:
cond = """and payroll_frequency = '%(payroll_frequency)s'""" % {"payroll_frequency": 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 parent from `tabSalary Structure Employee`
where employee=%s and (from_date <= %s or from_date <= %s) 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 (to_date is null or to_date >= %s or to_date >= %s)
and parent in (select name from `tabSalary Structure` and parent in (select name from `tabSalary Structure`
where is_active = 'Yes'%s) where is_active = 'Yes'%s)
"""% ('%s', '%s', '%s','%s','%s', cond),(self.employee, self.start_date, joining_date, self.end_date, relieving_date)) """% ('%s', '%s', '%s','%s','%s', cond),(self.employee, self.start_date, joining_date, self.end_date, relieving_date))
if st_name: if st_name:
if len(st_name) > 1: if len(st_name) > 1:
frappe.msgprint(_("Multiple active Salary Structures found for employee {0} for the given dates") frappe.msgprint(_("Multiple active Salary Structures found for employee {0} for the given dates")
.format(self.employee), title=_('Warning')) .format(self.employee), title=_('Warning'))
return st_name and st_name[0][0] or '' return st_name and st_name[0][0] or ''
else: else:
self.salary_structure = None self.salary_structure = None
frappe.msgprint(_("No active or default Salary Structure found for employee {0} for the given dates") frappe.msgprint(_("No active or default Salary Structure found for employee {0} for the given dates")
.format(self.employee), title=_('Salary Structure Missing')) .format(self.employee), title=_('Salary Structure Missing'))
def pull_sal_struct(self): def pull_sal_struct(self):
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
if self.salary_slip_based_on_timesheet: if self.salary_slip_based_on_timesheet:
self.salary_structure = self._salary_structure_doc.name self.salary_structure = self._salary_structure_doc.name
self.hour_rate = self._salary_structure_doc.hour_rate self.hour_rate = self._salary_structure_doc.hour_rate
self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0
wages_amount = self.hour_rate * self.total_working_hours wages_amount = self.hour_rate * self.total_working_hours
self.add_earning_for_hourly_wages(self, self._salary_structure_doc.salary_component, wages_amount) self.add_earning_for_hourly_wages(self, self._salary_structure_doc.salary_component, wages_amount)
make_salary_slip(self._salary_structure_doc.name, self) make_salary_slip(self._salary_structure_doc.name, self)
def process_salary_structure(self): def process_salary_structure(self):
'''Calculate salary after salary structure details have been updated''' '''Calculate salary after salary structure details have been updated'''
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
self.get_date_details() self.get_date_details()
self.pull_emp_details() self.pull_emp_details()
self.get_leave_details() self.get_leave_details()
self.calculate_net_pay() self.calculate_net_pay()
def add_earning_for_hourly_wages(self, doc, salary_component, amount): def add_earning_for_hourly_wages(self, doc, salary_component, amount):
row_exists = False row_exists = False
for row in doc.earnings: for row in doc.earnings:
if row.salary_component == salary_component: if row.salary_component == salary_component:
row.amount = amount row.amount = amount
row_exists = True row_exists = True
break break
if not row_exists: if not row_exists:
wages_row = { wages_row = {
"salary_component": salary_component, "salary_component": salary_component,
"abbr": frappe.db.get_value("Salary Component", salary_component, "salary_component_abbr"), "abbr": frappe.db.get_value("Salary Component", salary_component, "salary_component_abbr"),
"amount": self.hour_rate * self.total_working_hours "amount": self.hour_rate * self.total_working_hours
} }
doc.append('earnings', wages_row) doc.append('earnings', wages_row)
def pull_emp_details(self): def pull_emp_details(self):
emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no"], as_dict=1) emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no"], as_dict=1)
if emp: if emp:
self.bank_name = emp.bank_name self.bank_name = emp.bank_name
self.bank_account_no = emp.bank_ac_no self.bank_account_no = emp.bank_ac_no
def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None): def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None):
if not joining_date: if not joining_date:
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
holidays = self.get_holidays_for_employee(self.start_date, self.end_date) holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
working_days = date_diff(self.end_date, self.start_date) + 1 working_days = date_diff(self.end_date, self.start_date) + 1
if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")):
working_days -= len(holidays) working_days -= len(holidays)
if working_days < 0: if working_days < 0:
frappe.throw(_("There are more holidays than working days this month.")) frappe.throw(_("There are more holidays than working days this month."))
actual_lwp = self.calculate_lwp(holidays, working_days) actual_lwp = self.calculate_lwp(holidays, working_days)
if not lwp: if not lwp:
lwp = actual_lwp lwp = actual_lwp
elif lwp != actual_lwp: elif lwp != actual_lwp:
frappe.msgprint(_("Leave Without Pay does not match with approved Leave Application records")) frappe.msgprint(_("Leave Without Pay does not match with approved Leave Application records"))
self.total_working_days = working_days self.total_working_days = working_days
self.leave_without_pay = lwp self.leave_without_pay = lwp
payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp) payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp)
self.payment_days = payment_days > 0 and payment_days or 0 self.payment_days = payment_days > 0 and payment_days or 0
def get_payment_days(self, joining_date, relieving_date): def get_payment_days(self, joining_date, relieving_date):
start_date = getdate(self.start_date) start_date = getdate(self.start_date)
if joining_date: if joining_date:
if getdate(self.start_date) <= joining_date <= getdate(self.end_date): if getdate(self.start_date) <= joining_date <= getdate(self.end_date):
start_date = joining_date start_date = joining_date
elif joining_date > getdate(self.end_date): elif joining_date > getdate(self.end_date):
return return
end_date = getdate(self.end_date) end_date = getdate(self.end_date)
if relieving_date: if relieving_date:
if getdate(self.start_date) <= relieving_date <= getdate(self.end_date): if getdate(self.start_date) <= relieving_date <= getdate(self.end_date):
end_date = relieving_date end_date = relieving_date
elif relieving_date < getdate(self.start_date): elif relieving_date < getdate(self.start_date):
frappe.throw(_("Employee relieved on {0} must be set as 'Left'") frappe.throw(_("Employee relieved on {0} must be set as 'Left'")
.format(relieving_date)) .format(relieving_date))
payment_days = date_diff(end_date, start_date) + 1 payment_days = date_diff(end_date, start_date) + 1
if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")):
holidays = self.get_holidays_for_employee(start_date, end_date) holidays = self.get_holidays_for_employee(start_date, end_date)
payment_days -= len(holidays) payment_days -= len(holidays)
return payment_days return payment_days
def get_holidays_for_employee(self, start_date, end_date): def get_holidays_for_employee(self, start_date, end_date):
holiday_list = get_holiday_list_for_employee(self.employee) holiday_list = get_holiday_list_for_employee(self.employee)
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
where where
parent=%(holiday_list)s parent=%(holiday_list)s
and holiday_date >= %(start_date)s and holiday_date >= %(start_date)s
and holiday_date <= %(end_date)s''', { and holiday_date <= %(end_date)s''', {
"holiday_list": holiday_list, "holiday_list": holiday_list,
"start_date": start_date, "start_date": start_date,
"end_date": end_date "end_date": end_date
}) })
holidays = [cstr(i) for i in holidays] holidays = [cstr(i) for i in holidays]
return holidays return holidays
def calculate_lwp(self, holidays, working_days): def calculate_lwp(self, holidays, working_days):
lwp = 0 lwp = 0
holidays = "','".join(holidays) holidays = "','".join(holidays)
for d in range(working_days): for d in range(working_days):
dt = add_days(cstr(getdate(self.start_date)), d) dt = add_days(cstr(getdate(self.start_date)), d)
leave = frappe.db.sql(""" leave = frappe.db.sql("""
select t1.name, t1.half_day select t1.name, t1.half_day
from `tabLeave Application` t1, `tabLeave Type` t2 from `tabLeave Application` t1, `tabLeave Type` t2
where t2.name = t1.leave_type where t2.name = t1.leave_type
and t2.is_lwp = 1 and t2.is_lwp = 1
and t1.docstatus = 1 and t1.docstatus = 1
and t1.status = 'Approved' and t1.status = 'Approved'
and t1.employee = %(employee)s and t1.employee = %(employee)s
and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
END END
""".format(holidays), {"employee": self.employee, "dt": dt}) """.format(holidays), {"employee": self.employee, "dt": dt})
if leave: if leave:
lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
return lwp return lwp
def check_existing(self): def check_existing(self):
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
ret_exist = frappe.db.sql("""select name from `tabSalary Slip` ret_exist = frappe.db.sql("""select name from `tabSalary Slip`
where start_date = %s and end_date = %s and docstatus != 2 where start_date = %s and end_date = %s and docstatus != 2
and employee = %s and name != %s""", and employee = %s and name != %s""",
(self.start_date, self.end_date, self.employee, self.name)) (self.start_date, self.end_date, self.employee, self.name))
if ret_exist: if ret_exist:
self.employee = '' self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee)) frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
else: else:
for data in self.timesheets: for data in self.timesheets:
if frappe.db.get_value('Timesheet', data.time_sheet, 'status') == 'Payrolled': if frappe.db.get_value('Timesheet', data.time_sheet, 'status') == 'Payrolled':
frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet)) frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet))
def sum_components(self, component_type, total_field): def sum_components(self, component_type, total_field):
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
if not relieving_date: if not relieving_date:
relieving_date = getdate(self.end_date) relieving_date = getdate(self.end_date)
for d in self.get(component_type): for d in self.get(component_type):
if ((cint(d.depends_on_lwp) == 1 and not self.salary_slip_based_on_timesheet) or\ if ((cint(d.depends_on_lwp) == 1 and not self.salary_slip_based_on_timesheet) or\
getdate(self.start_date) < joining_date or getdate(self.end_date) > relieving_date): getdate(self.start_date) < joining_date or getdate(self.end_date) > relieving_date):
d.amount = rounded((flt(d.default_amount) * flt(self.payment_days) d.amount = rounded((flt(d.default_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), self.precision("amount", component_type)) / cint(self.total_working_days)), self.precision("amount", component_type))
elif not self.payment_days and not self.salary_slip_based_on_timesheet: elif not self.payment_days and not self.salary_slip_based_on_timesheet:
d.amount = 0 d.amount = 0
elif not d.amount: elif not d.amount:
d.amount = d.default_amount d.amount = d.default_amount
self.set(total_field, self.get(total_field) + flt(d.amount)) self.set(total_field, self.get(total_field) + flt(d.amount))
def calculate_net_pay(self): def calculate_net_pay(self):
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts() self.calculate_component_amounts()
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total")) disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total"))
self.total_deduction = 0 self.total_deduction = 0
self.gross_pay = 0 self.gross_pay = 0
self.sum_components('earnings', 'gross_pay') self.sum_components('earnings', 'gross_pay')
self.sum_components('deductions', 'total_deduction') self.sum_components('deductions', 'total_deduction')
self.set_loan_repayment() self.set_loan_repayment()
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay, self.rounded_total = rounded(self.net_pay,
self.precision("net_pay") if disable_rounded_total else 0) self.precision("net_pay") if disable_rounded_total else 0)
def set_loan_repayment(self): def set_loan_repayment(self):
employee_loan = frappe.db.sql("""select sum(principal_amount) as principal_amount, sum(interest_amount) as interest_amount, employee_loan = frappe.db.sql("""select sum(principal_amount) as principal_amount, sum(interest_amount) as interest_amount,
sum(total_payment) as total_loan_repayment from `tabRepayment Schedule` sum(total_payment) as total_loan_repayment from `tabRepayment Schedule`
where payment_date between %s and %s and parent in (select name from `tabEmployee Loan` where payment_date between %s and %s and parent in (select name from `tabEmployee Loan`
where employee = %s and repay_from_salary = 1 and docstatus = 1)""", where employee = %s and repay_from_salary = 1 and docstatus = 1)""",
(self.start_date, self.end_date, self.employee), as_dict=True) (self.start_date, self.end_date, self.employee), as_dict=True)
if employee_loan: if employee_loan:
self.principal_amount = employee_loan[0].principal_amount self.principal_amount = employee_loan[0].principal_amount
self.interest_amount = employee_loan[0].interest_amount self.interest_amount = employee_loan[0].interest_amount
self.total_loan_repayment = employee_loan[0].total_loan_repayment self.total_loan_repayment = employee_loan[0].total_loan_repayment
def on_submit(self): def on_submit(self):
if self.net_pay < 0: if self.net_pay < 0:
frappe.throw(_("Net Pay cannot be less than 0")) frappe.throw(_("Net Pay cannot be less than 0"))
else: else:
self.set_status() self.set_status()
self.update_status(self.name) self.update_status(self.name)
if(frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")): if(frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")):
self.email_salary_slip() self.email_salary_slip()
def on_cancel(self): def on_cancel(self):
self.set_status() self.set_status()
self.update_status() self.update_status()
def email_salary_slip(self): def email_salary_slip(self):
receiver = frappe.db.get_value("Employee", self.employee, "prefered_email") receiver = frappe.db.get_value("Employee", self.employee, "prefered_email")
if receiver: if receiver:
subj = 'Salary Slip - from {0} to {1}'.format(self.start_date, self.end_date) subj = 'Salary Slip - from {0} to {1}'.format(self.start_date, self.end_date)
frappe.sendmail([receiver], subject=subj, message = _("Please see attachment"), frappe.sendmail([receiver], subject=subj, message = _("Please see attachment"),
attachments=[frappe.attach_print(self.doctype, self.name, file_name=self.name)], reference_doctype= self.doctype, reference_name= self.name) attachments=[frappe.attach_print(self.doctype, self.name, file_name=self.name)], reference_doctype= self.doctype, reference_name= self.name)
else: else:
msgprint(_("{0}: Employee email not found, hence email not sent").format(self.employee_name)) msgprint(_("{0}: Employee email not found, hence email not sent").format(self.employee_name))
def update_status(self, salary_slip=None): def update_status(self, salary_slip=None):
for data in self.timesheets: for data in self.timesheets:
if data.time_sheet: if data.time_sheet:
timesheet = frappe.get_doc('Timesheet', data.time_sheet) timesheet = frappe.get_doc('Timesheet', data.time_sheet)
timesheet.salary_slip = salary_slip timesheet.salary_slip = salary_slip
timesheet.flags.ignore_validate_update_after_submit = True timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status() timesheet.set_status()
timesheet.save() timesheet.save()
def set_status(self, status=None): def set_status(self, status=None):
'''Get and update status''' '''Get and update status'''
if not status: if not status:
status = self.get_status() status = self.get_status()
self.db_set("status", status) self.db_set("status", status)
def get_status(self): def get_status(self):
if self.docstatus == 0: if self.docstatus == 0:
status = "Draft" status = "Draft"
elif self.docstatus == 1: elif self.docstatus == 1:
status = "Submitted" status = "Submitted"
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
return status return status
def unlink_ref_doc_from_salary_slip(ref_no): def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no)) where journal_entry=%s and docstatus < 2""", (ref_no))
if linked_ss: if linked_ss:
for ss in linked_ss: for ss in linked_ss:
ss_doc = frappe.get_doc("Salary Slip", 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", "")