diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index 7964078c7f0..794664add43 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -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); } -} + +}); diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 134098f2523..fd3c0efbc9b 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -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", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f79f0fe4180..e5b3fdf656a 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -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 = [] diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 60ea0f9895d..4d731c79f8c 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -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' diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 61f3d2c2798..4fda25d7327 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -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", diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index d5fdda80944..916289de593 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -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): diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 1acd6e342fd..971ec523c97 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -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 = { diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json index 8863a0415eb..110c6f74a7e 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json @@ -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",