feat: Overtime

This commit is contained in:
Anurag Mishra
2021-05-31 13:10:46 +05:30
parent 8f1850b21c
commit e4fd6d7763
8 changed files with 349 additions and 41 deletions

View File

@@ -1,15 +1,67 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
frappe.ui.form.on('Attendance', {
onload: function(frm) {
cur_frm.cscript.onload = function(doc, cdt, cdn) {
if(doc.__islocal) cur_frm.set_value("attendance_date", frappe.datetime.get_today());
}
frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type").then((r)=>{
if (!r) {
// for not fetching from Shift Type
delete cur_frm.fetch_dict["shift"];
}
});
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
if (frm.doc.__islocal) {
cur_frm.set_value("attendance_date", frappe.datetime.get_today());
}
frm.set_query("employee", ()=>{
return {
query: "erpnext.controllers.queries.employee_query"
};
});
},
employee: function(frm) {
if (frm.doc.employee) {
frm.events.set_shift(frm);
frm.events.set_overtime_type(frm);
}
},
set_shift: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.get_shift_type",
args: {
employee: frm.doc.employee,
attendance_date: frm.doc.attendance_date
},
callback: function(r) {
if (r.message) {
frm.set_value("shift", r.message);
}
}
});
},
set_overtime_type: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.get_overtime_type",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value("overtime_type", r.message);
}
}
});
},
overtime_duration: function(frm) {
let duration = frm.doc.overtime_duration.split(":");
let overtime_duration_words = duration[0] + " Hours " + duration[1] + " Minutes";
frm.set_value("overtime_duration_words", overtime_duration_words);
}
}
});

View File

@@ -11,7 +11,6 @@
"naming_series",
"employee",
"employee_name",
"working_hours",
"status",
"leave_type",
"leave_application",
@@ -20,13 +19,22 @@
"company",
"department",
"attendance_request",
"details_section",
"shift_details_section",
"shift",
"in_time",
"out_time",
"column_break_18",
"standard_working_time",
"standard_working_time_delta",
"working_time",
"working_timedelta",
"late_entry",
"early_exit",
"overtime_details_section",
"overtime_type",
"overtime_duration",
"column_break_27",
"overtime_duration_words",
"amended_from"
],
"fields": [
@@ -69,14 +77,6 @@
"oldfieldtype": "Data",
"read_only": 1
},
{
"depends_on": "working_hours",
"fieldname": "working_hours",
"fieldtype": "Float",
"label": "Working Hours",
"precision": "1",
"read_only": 1
},
{
"default": "Present",
"fieldname": "status",
@@ -125,6 +125,7 @@
"reqd": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -146,7 +147,8 @@
"fieldname": "shift",
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type"
"options": "Shift Type",
"read_only": 1
},
{
"fieldname": "attendance_request",
@@ -177,11 +179,6 @@
"fieldtype": "Check",
"label": "Early Exit"
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"depends_on": "shift",
"fieldname": "in_time",
@@ -199,13 +196,78 @@
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"depends_on": "overtime_type",
"fieldname": "overtime_details_section",
"fieldtype": "Section Break",
"label": "Overtime Details"
},
{
"depends_on": "overtime_type",
"fieldname": "overtime_type",
"fieldtype": "Link",
"label": "Overtime Type",
"options": "Overtime Type",
"read_only": 1
},
{
"description": "Shift duration for a day",
"fetch_from": "shift.standard_working_time",
"fieldname": "standard_working_time",
"fieldtype": "Data",
"label": " Standard Working Time",
"read_only": 1
},
{
"fetch_from": "shift.working_time_delta",
"fieldname": "standard_working_time_delta",
"fieldtype": "Time",
"hidden": 1,
"label": "Standard Working Time(Delta)"
},
{
"depends_on": "working_time",
"fieldname": "working_time",
"fieldtype": "Data",
"label": "Total Working Time",
"precision": "1",
"read_only": 1
},
{
"fieldname": "working_timedelta",
"fieldtype": "Time",
"hidden": 1,
"label": "Working Time(Delta)"
},
{
"fieldname": "overtime_duration_words",
"fieldtype": "Data",
"label": "Overtime Duration(Words)",
"read_only": 1
},
{
"default": "00:00:00",
"fieldname": "overtime_duration",
"fieldtype": "Time",
"label": "Overtime Duration"
},
{
"depends_on": "shift",
"fieldname": "shift_details_section",
"fieldtype": "Section Break",
"label": "Shift Details"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-ok",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-05-26 16:44:33.219313",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",

View File

@@ -7,8 +7,8 @@ import frappe
from frappe.utils import getdate, nowdate
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
from frappe.utils import cstr, get_datetime, formatdate, getdate
class Attendance(Document):
def validate(self):
@@ -19,6 +19,8 @@ class Attendance(Document):
self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record()
self.set_overtime_type()
self.set_default_shift()
def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
@@ -45,6 +47,13 @@ class Attendance(Document):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def set_default_shift(self):
if not self.shift:
self.shift = get_shift_type(self.employee, self.attendance_date)
def set_overtime_type(self):
self.overtime_type = get_overtime_type(self.employee)
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date
@@ -80,6 +89,63 @@ class Attendance(Document):
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
def calculate_overtime_duration(self):
#this method is only for Calculation of overtime based on Attendance through Employee Checkins
overtime_duration = self.working_timedelta - self.standard_working_time_delta
self.overtime_duration = overtime_duration
overtime_duration = str(overtime_duration).split(':')
if int(overtime_duration[0]) or int(overtime_duration[1]):
self.overtime_duration_words = overtime_duration[0] + " Hours " + overtime_duration[1] + " Minutes"
@frappe.whitelist()
def get_shift_type(employee, attendance_date):
emp_shift = frappe.db.get_value("Employee", employee, "default_shift")
shift_assignment = frappe.db.sql('''SELECT name, shift_type
FROM
`tabShift Assignment`
WHERE
docstatus = 1
AND employee = %(employee)s AND start_date <= %(attendance_date)s
AND (end_date >= %(attendance_date)s OR end_date IS null)
AND status = "Active"
''', {
"employee": employee,
"attendance_date": attendance_date,
}, as_dict = 1)
if len(shift_assignment):
shift = shift_assignment[0].shift_type
else:
shift = emp_shift
return shift
@frappe.whitelist()
def get_overtime_type(employee):
overtime_based_on = frappe.db.get_single_value("Payroll Settings", "overtime_based_on")
if overtime_based_on == "Attendance":
emp_department = frappe.db.get_value("Employee", employee, "department")
if emp_department:
overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Department", "party": emp_department}, fields=['name'])
if len(overtime_type):
overtime_type = overtime_type[0].name
emp_grade = frappe.db.get_value("Employee", employee, "grade")
if emp_grade:
overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Employee Grade", "party": emp_grade},
fields=['name'])
if len(overtime_type):
overtime_type = overtime_type[0].name
overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Employee", "party": employee}, fields=['name'])
if len(overtime_type):
overtime_type = overtime_type[0].name
return overtime_type
@frappe.whitelist()
def get_events(start, end, filters=None):
events = []

View File

@@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe
from frappe.utils import now, cint, get_datetime
from frappe.model.document import Document
from datetime import timedelta
from math import modf
from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
@@ -38,8 +40,12 @@ class EmployeeCheckin(Document):
self.shift_actual_end = shift_actual_timings[1]
self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_datetime
else:
self.shift = None
elif frappe.db.get_value("Shift Type", shift_actual_timings[2].shift_type.name, "allow_overtime"):
#because after Actual time it takes check-in/out invalid
#if employee checkout late or check-in before before shift timing adding time buffer.
self.shift = shift_actual_timings[2].shift_type.name
self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_datetime
@frappe.whitelist()
def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
@@ -56,7 +62,8 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
if not employee_field_value or not timestamp:
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True)
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value},
["name", "employee_name", employee_fieldname], as_dict=True)
if employee:
employee = employee[0]
else:
@@ -93,12 +100,21 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
elif attendance_status in ('Present', 'Absent', 'Half Day'):
employee_doc = frappe.get_doc('Employee', employee)
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
working_timedelta = '00:00:00'
working_time = None
working_time = modf(working_hours)
if working_time[1] or working_time[0]:
working_timedelta = timedelta(hours =int(working_time[1]), minutes = int(working_time[0] * 60))
working_time = str(int(working_time[1])) + ' Hours ' + str(int(working_time[0] * 60)) + ' Minutes'
doc_dict = {
'doctype': 'Attendance',
'employee': employee,
'attendance_date': attendance_date,
'status': attendance_status,
'working_hours': working_hours,
'working_time': working_time,
'working_timedelta': working_timedelta,
'company': employee_doc.company,
'shift': shift,
'late_entry': late_entry,
@@ -106,7 +122,11 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
'in_time': in_time,
'out_time': out_time
}
attendance = frappe.get_doc(doc_dict).insert()
if frappe.db.get_value("Shift type", shift, "allow_overtime"):
attendance.calculate_overtime_duration()
attendance.save()
attendance.submit()
frappe.db.sql("""update `tabEmployee Checkin`
set attendance = %s
@@ -121,10 +141,10 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "prompt",
"creation": "2018-04-13 16:22:52.954783",
"doctype": "DocType",
@@ -7,17 +8,22 @@
"field_order": [
"start_time",
"end_time",
"standard_working_time",
"working_time_delta",
"column_break_3",
"holiday_list",
"enable_auto_attendance",
"allow_overtime",
"auto_attendance_settings_section",
"determine_check_in_and_check_out",
"working_hours_calculation_based_on",
"column_break_10",
"begin_check_in_before_shift_start_time",
"allow_check_out_after_shift_end_time",
"column_break_10",
"section_break_15",
"working_hours_threshold_for_half_day",
"working_hours_threshold_for_absent",
"column_break_19",
"process_attendance_after",
"last_sync_of_checkin",
"grace_period_settings_auto_attendance_section",
@@ -29,6 +35,7 @@
],
"fields": [
{
"default": "00:00:00",
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
@@ -36,6 +43,7 @@
"reqd": 1
},
{
"default": "00:00:00",
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
@@ -84,6 +92,7 @@
},
{
"default": "60",
"depends_on": "eval: doc.allow_overtime == 0",
"description": "The time before the shift start time during which Employee Check-in is considered for attendance.",
"fieldname": "begin_check_in_before_shift_start_time",
"fieldtype": "Int",
@@ -121,6 +130,7 @@
},
{
"default": "60",
"depends_on": "eval: doc.allow_overtime == 0",
"description": "Time after the end of shift during which check-out is considered for attendance.",
"fieldname": "allow_check_out_after_shift_end_time",
"fieldtype": "Int",
@@ -156,9 +166,39 @@
"fieldname": "last_sync_of_checkin",
"fieldtype": "Datetime",
"label": "Last Sync of Checkin"
},
{
"fieldname": "standard_working_time",
"fieldtype": "Data",
"label": "Standard Working Time",
"read_only": 1
},
{
"fieldname": "working_time_delta",
"fieldtype": "Time",
"hidden": 1,
"label": "Working time(delta)"
},
{
"default": "0",
"depends_on": "enable_auto_attendance",
"description": "Overtime will be calculated and Overtime Duration will reflect in attendance records. Check Payroll Settings for more options. ",
"fieldname": "allow_overtime",
"fieldtype": "Check",
"label": "Allow Overtime"
},
{
"depends_on": "enable_auto_attendance",
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
}
],
"modified": "2019-07-30 01:05:24.660666",
"links": [],
"modified": "2021-05-26 14:10:09.574202",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Type",

View File

@@ -7,14 +7,50 @@ import itertools
from datetime import timedelta
import frappe
from frappe import _
from math import modf
from frappe.model.document import Document
from frappe.utils import cint, getdate, get_datetime
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift
from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from datetime import timedelta
class ShiftType(Document):
def validate(self):
self.validate_overtime()
self.set_working_hours()
def set_working_hours(self):
end_time = self.end_time.split(":")
start_time = self.start_time.split(":")
shift_end = timedelta(hours = int(end_time[0]), minutes = int(end_time[1]), seconds = int(end_time[2]))
shift_start = timedelta(hours =int(start_time[0]), minutes = int(start_time[1]), seconds = int(start_time[2]))
if shift_end > shift_start:
time_difference = shift_end - shift_start
else:
# for night shift
time_difference = shift_start - shift_end
self.working_time_delta = str(time_difference)
time_difference = str(time_difference).split(":")
if int(time_difference[0]) or int(time_difference[1]):
self.standard_working_time = time_difference[0] + " Hours " + time_difference[1] + " Minutes"
def validate_overtime(self):
if not frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type") and self.allow_overtime:
frappe.throw(_('Please enable "Fetch Standard Working Hours from Shift Type" in payroll Settings for Overtime.'))
if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") != "Attendance" and self.allow_overtime:
frappe.throw(_('Please set Overtime based on "Attendance" in payroll Settings for Overtime.'))
@frappe.whitelist()
def process_auto_attendance(self):
if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
@@ -27,10 +63,17 @@ class ShiftType(Document):
'shift': self.name
}
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
from pprint import pprint
pprint(logs)
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group)
attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs)
print("_______>>>>>>>>>>>>>",attendance_status, working_hours, late_entry, early_exit, in_time, out_time)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee)
@@ -41,8 +84,10 @@ class ShiftType(Document):
1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order
"""
late_entry = early_exit = False
total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
late_entry = True
@@ -51,8 +96,10 @@ class ShiftType(Document):
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
def mark_absent_for_dates_with_no_attendance(self, employee):

View File

@@ -235,11 +235,17 @@ def get_gratuity_rule_slabs(gratuity_rule):
return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
def get_salary_structure(employee):
return frappe.get_list("Salary Structure Assignment", filters = {
salary_struct = frappe.get_list("Salary Structure Assignment", filters = {
"employee": employee, 'docstatus': 1
},
fields=["from_date", "salary_structure"],
order_by = "from_date desc")[0].salary_structure
order_by = "from_date desc")
if len(salary_struct):
return salary_struct[0].salary_structure
else:
frappe.throw(_("No Salary Structure Assignment found for employee: {0}").format(employee))
def get_last_salary_slip(employee):
return frappe.get_list("Salary Slip", filters = {

View File

@@ -19,9 +19,11 @@
"password_policy",
"section_break_12",
"overtime_based_on",
"maximum_overtime_hours_allowed",
"overtime_salary_component",
"column_break_14",
"fetch_standard_working_hours_from_shift_type"
"fetch_standard_working_hours_from_shift_type",
"is_overtime_approval_required"
],
"fields": [
{
@@ -105,7 +107,8 @@
},
{
"fieldname": "section_break_12",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Overtime Calculation"
},
{
"default": "0",
@@ -124,13 +127,25 @@
"fieldtype": "Select",
"label": "Calculate Overtime Hours Based On",
"options": "Attendance\nTimesheet"
},
{
"default": "0",
"fieldname": "is_overtime_approval_required",
"fieldtype": "Check",
"label": "Is Overtime Approval Required"
},
{
"description": "Overtime payment will not be given for more than the defined hours limit.",
"fieldname": "maximum_overtime_hours_allowed",
"fieldtype": "Int",
"label": "Maximum Overtime Hours Allowed For Payment "
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-05-10 12:56:08.161319",
"modified": "2021-05-26 15:56:25.313007",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",