Payroll based on attendance (#21258)
* feat: Payroll based on attendance and leave * test: salary slip based 0n attendance * feat: Payroll based on attendance * fix: Codacy issues Co-authored-by: Anurag Mishra <mishranaman123@gmail.com>
This commit is contained in:
parent
d78cf97250
commit
ba70e7e8bc
@ -87,11 +87,12 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status==\"On Leave\"",
|
||||
"depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)",
|
||||
"fieldname": "leave_type",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Leave Type",
|
||||
"mandatory_depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)",
|
||||
"oldfieldname": "leave_type",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Leave Type"
|
||||
@ -100,6 +101,7 @@
|
||||
"fieldname": "leave_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Application",
|
||||
"no_copy": 1,
|
||||
"options": "Leave Application",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -175,7 +177,8 @@
|
||||
"icon": "fa fa-ok",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-02-19 14:25:32.945842",
|
||||
"links": [],
|
||||
"modified": "2020-04-11 11:40:14.319496",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Attendance",
|
||||
|
@ -7,33 +7,15 @@ import frappe
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, get_datetime, get_datetime_str
|
||||
from frappe.utils import update_progress_bar
|
||||
from frappe.utils import cstr, get_datetime, format_date
|
||||
|
||||
class Attendance(Document):
|
||||
def validate_duplicate_record(self):
|
||||
res = frappe.db.sql("""select name from `tabAttendance` where employee = %s and attendance_date = %s
|
||||
and name != %s and docstatus != 2""",
|
||||
(self.employee, getdate(self.attendance_date), self.name))
|
||||
if res:
|
||||
frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee))
|
||||
|
||||
def check_leave_record(self):
|
||||
leave_record = frappe.db.sql("""select leave_type, half_day, half_day_date from `tabLeave Application`
|
||||
where employee = %s and %s between from_date and to_date and status = 'Approved'
|
||||
and docstatus = 1""", (self.employee, self.attendance_date), as_dict=True)
|
||||
if leave_record:
|
||||
for d in leave_record:
|
||||
if d.half_day_date == getdate(self.attendance_date):
|
||||
self.status = 'Half Day'
|
||||
frappe.msgprint(_("Employee {0} on Half day on {1}").format(self.employee, self.attendance_date))
|
||||
else:
|
||||
self.status = 'On Leave'
|
||||
self.leave_type = d.leave_type
|
||||
frappe.msgprint(_("Employee {0} is on Leave on {1}").format(self.employee, self.attendance_date))
|
||||
|
||||
if self.status == "On Leave" and not leave_record:
|
||||
frappe.throw(_("No leave record found for employee {0} for {1}").format(self.employee, self.attendance_date))
|
||||
def validate(self):
|
||||
from erpnext.controllers.status_updater import validate_status
|
||||
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
||||
self.validate_attendance_date()
|
||||
self.validate_duplicate_record()
|
||||
self.check_leave_record()
|
||||
|
||||
def validate_attendance_date(self):
|
||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||
@ -44,19 +26,52 @@ class Attendance(Document):
|
||||
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
|
||||
frappe.throw(_("Attendance date can not be less than employee's joining date"))
|
||||
|
||||
def validate_duplicate_record(self):
|
||||
res = frappe.db.sql("""
|
||||
select name from `tabAttendance`
|
||||
where employee = %s
|
||||
and attendance_date = %s
|
||||
and name != %s
|
||||
and docstatus != 2
|
||||
""", (self.employee, getdate(self.attendance_date), self.name))
|
||||
if res:
|
||||
frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee))
|
||||
|
||||
def check_leave_record(self):
|
||||
leave_record = frappe.db.sql("""
|
||||
select leave_type, half_day, half_day_date
|
||||
from `tabLeave Application`
|
||||
where employee = %s
|
||||
and %s between from_date and to_date
|
||||
and status = 'Approved'
|
||||
and docstatus = 1
|
||||
""", (self.employee, self.attendance_date), as_dict=True)
|
||||
if leave_record:
|
||||
for d in leave_record:
|
||||
self.leave_type = d.leave_type
|
||||
if d.half_day_date == getdate(self.attendance_date):
|
||||
self.status = 'Half Day'
|
||||
frappe.msgprint(_("Employee {0} on Half day on {1}")
|
||||
.format(self.employee, format_date(self.attendance_date)))
|
||||
else:
|
||||
self.status = 'On Leave'
|
||||
frappe.msgprint(_("Employee {0} is on Leave on {1}")
|
||||
.format(self.employee, format_date(self.attendance_date)))
|
||||
|
||||
if self.status in ("On Leave", "Half Day"):
|
||||
if not leave_record:
|
||||
frappe.msgprint(_("No leave record found for employee {0} on {1}")
|
||||
.format(self.employee, format_date(self.attendance_date)), alert=1)
|
||||
elif self.leave_type:
|
||||
self.leave_type = None
|
||||
self.leave_application = None
|
||||
|
||||
def validate_employee(self):
|
||||
emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'",
|
||||
self.employee)
|
||||
if not emp:
|
||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||
|
||||
def validate(self):
|
||||
from erpnext.controllers.status_updater import validate_status
|
||||
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
||||
self.validate_attendance_date()
|
||||
self.validate_duplicate_record()
|
||||
self.check_leave_record()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
events = []
|
||||
@ -90,18 +105,20 @@ def add_attendance(events, start, end, conditions=None):
|
||||
if e not in events:
|
||||
events.append(e)
|
||||
|
||||
def mark_attendance(employee, attendance_date, status, shift=None):
|
||||
employee_doc = frappe.get_doc('Employee', employee)
|
||||
def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False):
|
||||
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
||||
doc_dict = {
|
||||
company = frappe.db.get_value('Employee', employee, 'company')
|
||||
attendance = frappe.get_doc({
|
||||
'doctype': 'Attendance',
|
||||
'employee': employee,
|
||||
'attendance_date': attendance_date,
|
||||
'status': status,
|
||||
'company': employee_doc.company,
|
||||
'shift': shift
|
||||
}
|
||||
attendance = frappe.get_doc(doc_dict).insert()
|
||||
'company': company,
|
||||
'shift': shift,
|
||||
'leave_type': leave_type
|
||||
})
|
||||
attendance.flags.ignore_validate = ignore_validate
|
||||
attendance.insert()
|
||||
attendance.submit()
|
||||
return attendance.name
|
||||
|
||||
|
@ -13,10 +13,12 @@
|
||||
"stop_birthday_reminders",
|
||||
"expense_approver_mandatory_in_expense_claim",
|
||||
"payroll_settings",
|
||||
"payroll_based_on",
|
||||
"max_working_hours_against_timesheet",
|
||||
"include_holidays_in_total_working_days",
|
||||
"disable_rounded_total",
|
||||
"max_working_hours_against_timesheet",
|
||||
"column_break_11",
|
||||
"daily_wages_fraction_for_half_day",
|
||||
"email_salary_slip_to_employee",
|
||||
"encrypt_salary_slips_in_emails",
|
||||
"password_policy",
|
||||
@ -184,13 +186,27 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Create Backdated Leave Application",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Leave",
|
||||
"fieldname": "payroll_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Calculate Working Days in Payroll based on",
|
||||
"options": "Leave\nAttendance"
|
||||
},
|
||||
{
|
||||
"default": "0.5",
|
||||
"description": "The fraction of daily wages to be paid for half-day attendance",
|
||||
"fieldname": "daily_wages_fraction_for_half_day",
|
||||
"fieldtype": "Float",
|
||||
"label": "Daily Wages Fraction for Half Day"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-01-06 18:46:30.189815",
|
||||
"modified": "2020-04-13 21:20:59.382394",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR Settings",
|
||||
|
@ -15,6 +15,9 @@ class HRSettings(Document):
|
||||
self.set_naming_series()
|
||||
self.validate_password_policy()
|
||||
|
||||
if not self.daily_wages_fraction_for_half_day:
|
||||
self.daily_wages_fraction_for_half_day = 0.5
|
||||
|
||||
def set_naming_series(self):
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
set_by_naming_series("Employee", "employee_number",
|
||||
|
@ -51,7 +51,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
},
|
||||
|
||||
end_date: function(frm) {
|
||||
frm.events.get_emp_and_leave_details(frm);
|
||||
frm.events.get_emp_and_working_day_details(frm);
|
||||
},
|
||||
|
||||
set_end_date: function(frm){
|
||||
@ -86,7 +86,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
|
||||
salary_slip_based_on_timesheet: function(frm) {
|
||||
frm.trigger("toggle_fields");
|
||||
frm.events.get_emp_and_leave_details(frm);
|
||||
frm.events.get_emp_and_working_day_details(frm);
|
||||
},
|
||||
|
||||
payroll_frequency: function(frm) {
|
||||
@ -95,15 +95,14 @@ frappe.ui.form.on("Salary Slip", {
|
||||
},
|
||||
|
||||
employee: function(frm) {
|
||||
frm.events.get_emp_and_leave_details(frm);
|
||||
frm.events.get_emp_and_working_day_details(frm);
|
||||
},
|
||||
|
||||
leave_without_pay: function(frm){
|
||||
if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
|
||||
return frappe.call({
|
||||
method: 'process_salary_based_on_leave',
|
||||
method: 'process_salary_based_on_working_days',
|
||||
doc: frm.doc,
|
||||
args: {"lwp": frm.doc.leave_without_pay},
|
||||
callback: function(r, rt) {
|
||||
frm.refresh();
|
||||
}
|
||||
@ -115,12 +114,12 @@ frappe.ui.form.on("Salary Slip", {
|
||||
frm.toggle_display(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1);
|
||||
|
||||
frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'],
|
||||
frm.doc.payroll_frequency!="");
|
||||
frm.doc.payroll_frequency != "");
|
||||
},
|
||||
|
||||
get_emp_and_leave_details: function(frm) {
|
||||
get_emp_and_working_day_details: function(frm) {
|
||||
return frappe.call({
|
||||
method: 'get_emp_and_leave_details',
|
||||
method: 'get_emp_and_working_day_details',
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
frm.refresh();
|
||||
|
@ -11,20 +11,20 @@
|
||||
"employee_name",
|
||||
"department",
|
||||
"designation",
|
||||
"branch",
|
||||
"column_break1",
|
||||
"company",
|
||||
"status",
|
||||
"journal_entry",
|
||||
"payroll_entry",
|
||||
"company",
|
||||
"letter_head",
|
||||
"branch",
|
||||
"status",
|
||||
"section_break_10",
|
||||
"salary_slip_based_on_timesheet",
|
||||
"payroll_frequency",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"column_break_15",
|
||||
"salary_structure",
|
||||
"payroll_frequency",
|
||||
"total_working_days",
|
||||
"leave_without_pay",
|
||||
"payment_days",
|
||||
@ -309,6 +309,7 @@
|
||||
{
|
||||
"fieldname": "earning",
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Earning",
|
||||
"oldfieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
@ -323,6 +324,7 @@
|
||||
{
|
||||
"fieldname": "deduction",
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Deduction",
|
||||
"oldfieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
@ -463,7 +465,7 @@
|
||||
"idx": 9,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-09 20:02:53.159827",
|
||||
"modified": "2020-04-14 20:02:53.159827",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Salary Slip",
|
||||
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
import datetime, math
|
||||
|
||||
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words
|
||||
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date
|
||||
from frappe.model.naming import make_autoname
|
||||
|
||||
from frappe import msgprint, _
|
||||
@ -44,9 +44,9 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if not (len(self.get("earnings")) or len(self.get("deductions"))):
|
||||
# get details from salary structure
|
||||
self.get_emp_and_leave_details()
|
||||
self.get_emp_and_working_day_details()
|
||||
else:
|
||||
self.get_leave_details(lwp = self.leave_without_pay)
|
||||
self.get_working_days_details(lwp = self.leave_without_pay)
|
||||
|
||||
self.calculate_net_pay()
|
||||
|
||||
@ -117,7 +117,7 @@ class SalarySlip(TransactionBase):
|
||||
self.start_date = date_details.start_date
|
||||
self.end_date = date_details.end_date
|
||||
|
||||
def get_emp_and_leave_details(self):
|
||||
def get_emp_and_working_day_details(self):
|
||||
'''First time, load all the components from salary structure'''
|
||||
if self.employee:
|
||||
self.set("earnings", [])
|
||||
@ -129,7 +129,8 @@ class SalarySlip(TransactionBase):
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
|
||||
self.get_leave_details(joining_date, relieving_date)
|
||||
#getin leave details
|
||||
self.get_working_days_details(joining_date, relieving_date)
|
||||
struct = self.check_sal_struct(joining_date, relieving_date)
|
||||
|
||||
if struct:
|
||||
@ -188,10 +189,9 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
make_salary_slip(self._salary_structure_doc.name, self)
|
||||
|
||||
def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0):
|
||||
if not joining_date:
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0):
|
||||
payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on")
|
||||
include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days")
|
||||
|
||||
working_days = date_diff(self.end_date, self.start_date) + 1
|
||||
if for_preview:
|
||||
@ -200,24 +200,42 @@ class SalarySlip(TransactionBase):
|
||||
return
|
||||
|
||||
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
|
||||
actual_lwp = self.calculate_lwp(holidays, working_days)
|
||||
if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")):
|
||||
|
||||
if not cint(include_holidays_in_total_working_days):
|
||||
working_days -= len(holidays)
|
||||
if working_days < 0:
|
||||
frappe.throw(_("There are more holidays than working days this month."))
|
||||
|
||||
if not payroll_based_on:
|
||||
frappe.throw(_("Please set Payroll based on in HR settings"))
|
||||
|
||||
if payroll_based_on == "Attendance":
|
||||
actual_lwp = self.calculate_lwp_based_on_attendance(holidays)
|
||||
else:
|
||||
actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days)
|
||||
|
||||
if not lwp:
|
||||
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 {} records")
|
||||
.format(payroll_based_on))
|
||||
|
||||
self.total_working_days = working_days
|
||||
self.leave_without_pay = lwp
|
||||
self.total_working_days = working_days
|
||||
|
||||
payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp)
|
||||
self.payment_days = payment_days > 0 and payment_days or 0
|
||||
payment_days = self.get_payment_days(joining_date,
|
||||
relieving_date, include_holidays_in_total_working_days)
|
||||
|
||||
if flt(payment_days) > flt(lwp):
|
||||
self.payment_days = flt(payment_days) - flt(lwp)
|
||||
else:
|
||||
self.payment_days = 0
|
||||
|
||||
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
|
||||
if not joining_date:
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
|
||||
def get_payment_days(self, joining_date, relieving_date):
|
||||
start_date = getdate(self.start_date)
|
||||
if joining_date:
|
||||
if getdate(self.start_date) <= joining_date <= getdate(self.end_date):
|
||||
@ -235,9 +253,10 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
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(include_holidays_in_total_working_days):
|
||||
holidays = self.get_holidays_for_employee(start_date, end_date)
|
||||
payment_days -= len(holidays)
|
||||
|
||||
return payment_days
|
||||
|
||||
def get_holidays_for_employee(self, start_date, end_date):
|
||||
@ -256,27 +275,67 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
return holidays
|
||||
|
||||
def calculate_lwp(self, holidays, working_days):
|
||||
def calculate_lwp_based_on_leave_application(self, holidays, working_days):
|
||||
lwp = 0
|
||||
holidays = "','".join(holidays)
|
||||
daily_wages_fraction_for_half_day = \
|
||||
flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
||||
|
||||
for d in range(working_days):
|
||||
dt = add_days(cstr(getdate(self.start_date)), d)
|
||||
leave = frappe.db.sql("""
|
||||
SELECT t1.name,
|
||||
CASE WHEN t1.half_day_date = %(dt)s or t1.to_date = t1.from_date
|
||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
||||
THEN t1.half_day else 0 END
|
||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
||||
WHERE t2.name = t1.leave_type
|
||||
AND t2.is_lwp = 1
|
||||
AND t1.docstatus = 1
|
||||
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 ifnull(t1.salary_slip, '') = ''
|
||||
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = ''
|
||||
END
|
||||
AND ifnull(t1.salary_slip, '') = ''
|
||||
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
|
||||
END
|
||||
""".format(holidays), {"employee": self.employee, "dt": dt})
|
||||
|
||||
if leave:
|
||||
lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
|
||||
is_half_day_leave = cint(leave[0][1])
|
||||
lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||
|
||||
return lwp
|
||||
|
||||
def calculate_lwp_based_on_attendance(self, holidays):
|
||||
lwp = 0
|
||||
|
||||
daily_wages_fraction_for_half_day = \
|
||||
flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
||||
|
||||
lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1))
|
||||
|
||||
attendances = frappe.db.sql('''
|
||||
SELECT attendance_date, status, leave_type
|
||||
FROM `tabAttendance`
|
||||
WHERE
|
||||
status in ("Absent", "Half Day", "On leave")
|
||||
AND employee = %s
|
||||
AND docstatus = 1
|
||||
AND attendance_date between %s and %s
|
||||
''', values=(self.employee, self.start_date, self.end_date), as_dict=1)
|
||||
|
||||
for d in attendances:
|
||||
if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types:
|
||||
continue
|
||||
|
||||
if format_date(d.attendance_date, "yyyy-mm-dd") in holidays:
|
||||
if d.status == "Absent" or \
|
||||
(d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]):
|
||||
continue
|
||||
|
||||
lwp += (1 - daily_wages_fraction_for_half_day) if d.status == "Half Day" else 1
|
||||
|
||||
return lwp
|
||||
|
||||
def add_earning_for_hourly_wages(self, doc, salary_component, amount):
|
||||
@ -945,7 +1004,7 @@ class SalarySlip(TransactionBase):
|
||||
if not self.salary_slip_based_on_timesheet:
|
||||
self.get_date_details()
|
||||
self.pull_emp_details()
|
||||
self.get_leave_details(for_preview=for_preview)
|
||||
self.get_working_days_details(for_preview=for_preview)
|
||||
self.calculate_net_pay()
|
||||
|
||||
def pull_emp_details(self):
|
||||
@ -954,8 +1013,8 @@ class SalarySlip(TransactionBase):
|
||||
self.bank_name = emp.bank_name
|
||||
self.bank_account_no = emp.bank_ac_no
|
||||
|
||||
def process_salary_based_on_leave(self, lwp=0):
|
||||
self.get_leave_details(lwp=lwp)
|
||||
def process_salary_based_on_working_days(self):
|
||||
self.get_working_days_details(lwp=self.leave_without_pay)
|
||||
self.calculate_net_pay()
|
||||
|
||||
def unlink_ref_doc_from_salary_slip(ref_no):
|
||||
|
@ -21,18 +21,105 @@ class TestSalarySlip(unittest.TestCase):
|
||||
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
|
||||
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
self.make_holiday_list()
|
||||
|
||||
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
|
||||
frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0)
|
||||
|
||||
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
|
||||
frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_payment_days_based_on_attendance(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
no_of_days = self.get_no_of_days()
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("HR Settings", None, "payroll_based_on", "Attendance")
|
||||
frappe.db.set_value("HR Settings", None, "daily_wages_fraction_for_half_day", 0.75)
|
||||
|
||||
emp_id = make_employee("test_for_attendance@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||
|
||||
month_start_date = get_first_day(nowdate())
|
||||
month_end_date = get_last_day(nowdate())
|
||||
|
||||
first_sunday = frappe.db.sql("""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = 'Salary Slip Test Holiday List'
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""", (month_start_date, month_end_date))[0][0]
|
||||
|
||||
mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp
|
||||
mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # valid lwp
|
||||
mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp
|
||||
mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp
|
||||
mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp
|
||||
mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp
|
||||
|
||||
ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly")
|
||||
|
||||
self.assertEqual(ss.leave_without_pay, 2.25)
|
||||
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25)
|
||||
|
||||
#Gross pay calculation based on attendances
|
||||
gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay))
|
||||
|
||||
self.assertEqual(ss.gross_pay, gross_pay)
|
||||
|
||||
frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
def test_payment_days_based_on_leave_application(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
emp_id = make_employee("test_for_attendance@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||
|
||||
month_start_date = get_first_day(nowdate())
|
||||
month_end_date = get_last_day(nowdate())
|
||||
|
||||
first_sunday = frappe.db.sql("""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = 'Salary Slip Test Holiday List'
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""", (month_start_date, month_end_date))[0][0]
|
||||
|
||||
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
|
||||
|
||||
ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly")
|
||||
|
||||
self.assertEqual(ss.leave_without_pay, 3)
|
||||
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3)
|
||||
|
||||
#Gross pay calculation based on attendances
|
||||
gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay))
|
||||
|
||||
self.assertEqual(ss.gross_pay, gross_pay)
|
||||
|
||||
frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
def test_salary_slip_with_holidays_included(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1)
|
||||
@ -315,7 +402,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
|
||||
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
if not salary_structure:
|
||||
@ -603,3 +689,17 @@ def create_additional_salary(employee, payroll_period, amount):
|
||||
"type": "Earning"
|
||||
}).submit()
|
||||
return salary_date
|
||||
|
||||
def make_leave_application(employee, from_date, to_date, leave_type, company=None):
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype = 'Leave Application',
|
||||
employee = employee,
|
||||
leave_type = leave_type,
|
||||
from_date = from_date,
|
||||
to_date = to_date,
|
||||
company = company or erpnext.get_default_company() or "_Test Company",
|
||||
docstatus = 1,
|
||||
status = "Approved",
|
||||
leave_approver = 'test@example.com'
|
||||
))
|
||||
leave_application.submit()
|
@ -669,6 +669,7 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes
|
||||
erpnext.patches.v12_0.set_total_batch_quantity
|
||||
erpnext.patches.v12_0.rename_mws_settings_fields
|
||||
erpnext.patches.v12_0.set_updated_purpose_in_pick_list
|
||||
erpnext.patches.v12_0.set_default_payroll_based_on
|
||||
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
||||
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
|
||||
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
|
||||
|
6
erpnext/patches/v12_0/set_default_payroll_based_on.py
Normal file
6
erpnext/patches/v12_0/set_default_payroll_based_on.py
Normal file
@ -0,0 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("hr", "doctype", "hr_settings")
|
||||
frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave")
|
Loading…
x
Reference in New Issue
Block a user