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
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.status==\"On Leave\"",
|
"depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)",
|
||||||
"fieldname": "leave_type",
|
"fieldname": "leave_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Leave Type",
|
"label": "Leave Type",
|
||||||
|
"mandatory_depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)",
|
||||||
"oldfieldname": "leave_type",
|
"oldfieldname": "leave_type",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Leave Type"
|
"options": "Leave Type"
|
||||||
@ -100,6 +101,7 @@
|
|||||||
"fieldname": "leave_application",
|
"fieldname": "leave_application",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Leave Application",
|
"label": "Leave Application",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Leave Application",
|
"options": "Leave Application",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -175,7 +177,8 @@
|
|||||||
"icon": "fa fa-ok",
|
"icon": "fa fa-ok",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"modified": "2020-02-19 14:25:32.945842",
|
"links": [],
|
||||||
|
"modified": "2020-04-11 11:40:14.319496",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Attendance",
|
"name": "Attendance",
|
||||||
|
|||||||
@ -7,33 +7,15 @@ import frappe
|
|||||||
from frappe.utils import getdate, nowdate
|
from frappe.utils import getdate, nowdate
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, get_datetime, get_datetime_str
|
from frappe.utils import cstr, get_datetime, format_date
|
||||||
from frappe.utils import update_progress_bar
|
|
||||||
|
|
||||||
class Attendance(Document):
|
class Attendance(Document):
|
||||||
def validate_duplicate_record(self):
|
def validate(self):
|
||||||
res = frappe.db.sql("""select name from `tabAttendance` where employee = %s and attendance_date = %s
|
from erpnext.controllers.status_updater import validate_status
|
||||||
and name != %s and docstatus != 2""",
|
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
||||||
(self.employee, getdate(self.attendance_date), self.name))
|
self.validate_attendance_date()
|
||||||
if res:
|
self.validate_duplicate_record()
|
||||||
frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee))
|
self.check_leave_record()
|
||||||
|
|
||||||
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_attendance_date(self):
|
def validate_attendance_date(self):
|
||||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
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):
|
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"))
|
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):
|
def validate_employee(self):
|
||||||
emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'",
|
emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'",
|
||||||
self.employee)
|
self.employee)
|
||||||
if not emp:
|
if not emp:
|
||||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
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()
|
@frappe.whitelist()
|
||||||
def get_events(start, end, filters=None):
|
def get_events(start, end, filters=None):
|
||||||
events = []
|
events = []
|
||||||
@ -90,18 +105,20 @@ def add_attendance(events, start, end, conditions=None):
|
|||||||
if e not in events:
|
if e not in events:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
|
||||||
def mark_attendance(employee, attendance_date, status, shift=None):
|
def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False):
|
||||||
employee_doc = frappe.get_doc('Employee', employee)
|
|
||||||
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
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',
|
'doctype': 'Attendance',
|
||||||
'employee': employee,
|
'employee': employee,
|
||||||
'attendance_date': attendance_date,
|
'attendance_date': attendance_date,
|
||||||
'status': status,
|
'status': status,
|
||||||
'company': employee_doc.company,
|
'company': company,
|
||||||
'shift': shift
|
'shift': shift,
|
||||||
}
|
'leave_type': leave_type
|
||||||
attendance = frappe.get_doc(doc_dict).insert()
|
})
|
||||||
|
attendance.flags.ignore_validate = ignore_validate
|
||||||
|
attendance.insert()
|
||||||
attendance.submit()
|
attendance.submit()
|
||||||
return attendance.name
|
return attendance.name
|
||||||
|
|
||||||
|
|||||||
@ -13,10 +13,12 @@
|
|||||||
"stop_birthday_reminders",
|
"stop_birthday_reminders",
|
||||||
"expense_approver_mandatory_in_expense_claim",
|
"expense_approver_mandatory_in_expense_claim",
|
||||||
"payroll_settings",
|
"payroll_settings",
|
||||||
|
"payroll_based_on",
|
||||||
|
"max_working_hours_against_timesheet",
|
||||||
"include_holidays_in_total_working_days",
|
"include_holidays_in_total_working_days",
|
||||||
"disable_rounded_total",
|
"disable_rounded_total",
|
||||||
"max_working_hours_against_timesheet",
|
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
|
"daily_wages_fraction_for_half_day",
|
||||||
"email_salary_slip_to_employee",
|
"email_salary_slip_to_employee",
|
||||||
"encrypt_salary_slips_in_emails",
|
"encrypt_salary_slips_in_emails",
|
||||||
"password_policy",
|
"password_policy",
|
||||||
@ -184,13 +186,27 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Create Backdated Leave Application",
|
"label": "Role Allowed to Create Backdated Leave Application",
|
||||||
"options": "Role"
|
"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",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-01-06 18:46:30.189815",
|
"modified": "2020-04-13 21:20:59.382394",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"name": "HR Settings",
|
||||||
|
|||||||
@ -15,6 +15,9 @@ class HRSettings(Document):
|
|||||||
self.set_naming_series()
|
self.set_naming_series()
|
||||||
self.validate_password_policy()
|
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):
|
def set_naming_series(self):
|
||||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||||
set_by_naming_series("Employee", "employee_number",
|
set_by_naming_series("Employee", "employee_number",
|
||||||
|
|||||||
@ -51,7 +51,7 @@ frappe.ui.form.on("Salary Slip", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
end_date: function(frm) {
|
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){
|
set_end_date: function(frm){
|
||||||
@ -86,7 +86,7 @@ frappe.ui.form.on("Salary Slip", {
|
|||||||
|
|
||||||
salary_slip_based_on_timesheet: function(frm) {
|
salary_slip_based_on_timesheet: function(frm) {
|
||||||
frm.trigger("toggle_fields");
|
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) {
|
payroll_frequency: function(frm) {
|
||||||
@ -95,15 +95,14 @@ frappe.ui.form.on("Salary Slip", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
employee: function(frm) {
|
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){
|
leave_without_pay: function(frm){
|
||||||
if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
|
if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: 'process_salary_based_on_leave',
|
method: 'process_salary_based_on_working_days',
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
args: {"lwp": frm.doc.leave_without_pay},
|
|
||||||
callback: function(r, rt) {
|
callback: function(r, rt) {
|
||||||
frm.refresh();
|
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(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1);
|
||||||
|
|
||||||
frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'],
|
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({
|
return frappe.call({
|
||||||
method: 'get_emp_and_leave_details',
|
method: 'get_emp_and_working_day_details',
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: function(r, rt) {
|
callback: function(r, rt) {
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
|
|||||||
@ -11,20 +11,20 @@
|
|||||||
"employee_name",
|
"employee_name",
|
||||||
"department",
|
"department",
|
||||||
"designation",
|
"designation",
|
||||||
|
"branch",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"company",
|
"status",
|
||||||
"journal_entry",
|
"journal_entry",
|
||||||
"payroll_entry",
|
"payroll_entry",
|
||||||
|
"company",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"branch",
|
|
||||||
"status",
|
|
||||||
"section_break_10",
|
"section_break_10",
|
||||||
"salary_slip_based_on_timesheet",
|
"salary_slip_based_on_timesheet",
|
||||||
"payroll_frequency",
|
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"salary_structure",
|
"salary_structure",
|
||||||
|
"payroll_frequency",
|
||||||
"total_working_days",
|
"total_working_days",
|
||||||
"leave_without_pay",
|
"leave_without_pay",
|
||||||
"payment_days",
|
"payment_days",
|
||||||
@ -309,6 +309,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "earning",
|
"fieldname": "earning",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
|
"label": "Earning",
|
||||||
"oldfieldtype": "Column Break",
|
"oldfieldtype": "Column Break",
|
||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
@ -323,6 +324,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "deduction",
|
"fieldname": "deduction",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
|
"label": "Deduction",
|
||||||
"oldfieldtype": "Column Break",
|
"oldfieldtype": "Column Break",
|
||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
@ -463,7 +465,7 @@
|
|||||||
"idx": 9,
|
"idx": 9,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-04-09 20:02:53.159827",
|
"modified": "2020-04-14 20:02:53.159827",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Salary Slip",
|
"name": "Salary Slip",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
import datetime, math
|
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.model.naming import make_autoname
|
||||||
|
|
||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
@ -44,9 +44,9 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
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_working_day_details()
|
||||||
else:
|
else:
|
||||||
self.get_leave_details(lwp = self.leave_without_pay)
|
self.get_working_days_details(lwp = self.leave_without_pay)
|
||||||
|
|
||||||
self.calculate_net_pay()
|
self.calculate_net_pay()
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class SalarySlip(TransactionBase):
|
|||||||
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 get_emp_and_leave_details(self):
|
def get_emp_and_working_day_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", [])
|
||||||
@ -129,7 +129,8 @@ class SalarySlip(TransactionBase):
|
|||||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||||
["date_of_joining", "relieving_date"])
|
["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)
|
struct = self.check_sal_struct(joining_date, relieving_date)
|
||||||
|
|
||||||
if struct:
|
if struct:
|
||||||
@ -188,10 +189,9 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
make_salary_slip(self._salary_structure_doc.name, self)
|
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):
|
def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0):
|
||||||
if not joining_date:
|
payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on")
|
||||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days")
|
||||||
["date_of_joining", "relieving_date"])
|
|
||||||
|
|
||||||
working_days = date_diff(self.end_date, self.start_date) + 1
|
working_days = date_diff(self.end_date, self.start_date) + 1
|
||||||
if for_preview:
|
if for_preview:
|
||||||
@ -200,24 +200,42 @@ class SalarySlip(TransactionBase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
|
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)
|
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."))
|
||||||
|
|
||||||
|
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:
|
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 {} records")
|
||||||
|
.format(payroll_based_on))
|
||||||
|
|
||||||
self.total_working_days = working_days
|
|
||||||
self.leave_without_pay = lwp
|
self.leave_without_pay = lwp
|
||||||
|
self.total_working_days = working_days
|
||||||
|
|
||||||
payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp)
|
payment_days = self.get_payment_days(joining_date,
|
||||||
self.payment_days = payment_days > 0 and payment_days or 0
|
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)
|
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):
|
||||||
@ -235,9 +253,10 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
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(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):
|
||||||
@ -256,27 +275,67 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
return holidays
|
return holidays
|
||||||
|
|
||||||
def calculate_lwp(self, holidays, working_days):
|
def calculate_lwp_based_on_leave_application(self, holidays, working_days):
|
||||||
lwp = 0
|
lwp = 0
|
||||||
holidays = "','".join(holidays)
|
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):
|
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,
|
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
|
THEN t1.half_day else 0 END
|
||||||
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.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 ifnull(t1.salary_slip, '') = ''
|
AND ifnull(t1.salary_slip, '') = ''
|
||||||
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = ''
|
AND CASE
|
||||||
END
|
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})
|
""".format(holidays), {"employee": self.employee, "dt": dt})
|
||||||
|
|
||||||
if leave:
|
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
|
return lwp
|
||||||
|
|
||||||
def add_earning_for_hourly_wages(self, doc, salary_component, amount):
|
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:
|
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(for_preview=for_preview)
|
self.get_working_days_details(for_preview=for_preview)
|
||||||
self.calculate_net_pay()
|
self.calculate_net_pay()
|
||||||
|
|
||||||
def pull_emp_details(self):
|
def pull_emp_details(self):
|
||||||
@ -954,8 +1013,8 @@ class SalarySlip(TransactionBase):
|
|||||||
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 process_salary_based_on_leave(self, lwp=0):
|
def process_salary_based_on_working_days(self):
|
||||||
self.get_leave_details(lwp=lwp)
|
self.get_working_days_details(lwp=self.leave_without_pay)
|
||||||
self.calculate_net_pay()
|
self.calculate_net_pay()
|
||||||
|
|
||||||
def unlink_ref_doc_from_salary_slip(ref_no):
|
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_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||||
make_deduction_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)
|
frappe.db.sql("delete from `tab%s`" % dt)
|
||||||
|
|
||||||
self.make_holiday_list()
|
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("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, "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):
|
def tearDown(self):
|
||||||
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
|
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
|
||||||
frappe.set_user("Administrator")
|
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):
|
def test_salary_slip_with_holidays_included(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1)
|
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]
|
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||||
|
|
||||||
|
|
||||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||||
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
if not salary_structure:
|
if not salary_structure:
|
||||||
@ -603,3 +689,17 @@ def create_additional_salary(employee, payroll_period, amount):
|
|||||||
"type": "Earning"
|
"type": "Earning"
|
||||||
}).submit()
|
}).submit()
|
||||||
return salary_date
|
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.set_total_batch_quantity
|
||||||
erpnext.patches.v12_0.rename_mws_settings_fields
|
erpnext.patches.v12_0.rename_mws_settings_fields
|
||||||
erpnext.patches.v12_0.set_updated_purpose_in_pick_list
|
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.repost_stock_ledger_entries_for_target_warehouse
|
||||||
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
|
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
|
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