Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Over time #2314

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3e80609
feat: overtime feature
iamejaaz Oct 20, 2024
526163f
feat: update overtime slips on submit and cancel
iamejaaz Oct 20, 2024
622fe24
refactor: add translaction and use list comprehension
iamejaaz Oct 20, 2024
8708272
Merge branch 'develop' into over-time
iamejaaz Oct 20, 2024
1661c0a
Merge branch 'develop' into over-time
iamejaaz Oct 22, 2024
dc62310
Merge branch 'develop' into over-time
iamejaaz Oct 23, 2024
6b8f901
Merge branch 'develop' into over-time
iamejaaz Oct 24, 2024
6a66206
feat: add validation for max days allowed
iamejaaz Oct 25, 2024
612f687
test: write overtime slip test case
iamejaaz Oct 27, 2024
42a49b0
test: write test case for salary slip Overtime
iamejaaz Oct 27, 2024
73db262
test: add overtime allowance salary component
iamejaaz Oct 27, 2024
9acdabd
Merge branch 'develop' into over-time
iamejaaz Oct 27, 2024
5da32b5
Merge branch 'develop' into over-time
iamejaaz Oct 28, 2024
7fc5635
Merge branch 'develop' into over-time
iamejaaz Oct 29, 2024
be5acd8
Merge branch 'develop' into over-time
iamejaaz Oct 30, 2024
6b6158d
Merge branch 'develop' into over-time
iamejaaz Oct 30, 2024
779e325
refactor: return if maximum OT hours does not exists
iamejaaz Oct 31, 2024
fb8d26f
test: fix shift and overtime issue
iamejaaz Oct 31, 2024
44c9c53
Merge branch 'develop' into over-time
iamejaaz Nov 1, 2024
887a13d
refactor: calculate overtime hours dynamically
iamejaaz Nov 2, 2024
173b1c3
refactor: break big fucntion into small function
iamejaaz Nov 22, 2024
b800049
refactor: break down set_overtime_types_details into smaller functions
iamejaaz Nov 22, 2024
332849f
Merge branch 'develop' into over-time
iamejaaz Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hrms/hr/doctype/attendance/attendance.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,23 @@ frappe.ui.form.on("Attendance", {
};
});
},
employee: function (frm) {
if (frm.doc.employee && frm.doc.attendance_date) {
frm.events.set_shift(frm);
}
},
set_shift: function (frm) {
frappe.call({
method: "hrms.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);
}
},
});
},
});
47 changes: 45 additions & 2 deletions hrms/hr/doctype/attendance/attendance.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
"column_break_18",
"late_entry",
"early_exit",
"amended_from"
"amended_from",
"overtime_section",
"overtime_duration",
"overtime_type",
"column_break_idku",
"standard_working_hours",
"actual_overtime_duration"
],
"fields": [
{
Expand Down Expand Up @@ -201,13 +207,50 @@
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "overtime_section",
"fieldtype": "Section Break",
"label": "Overtime"
},
{
"default": "0",
"description": "Based on \"Maximum Overtime Hours Allowed\" in Overtime Type",
"fieldname": "overtime_duration",
"fieldtype": "Time",
"hide_days": 1,
"label": "Overtime Duration"
},
{
"fetch_from": "shift.overtime_type",
"fieldname": "overtime_type",
"fieldtype": "Link",
"label": "Overtime Type",
"options": "Overtime Type"
},
{
"fieldname": "column_break_idku",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "standard_working_hours",
"fieldtype": "Time",
"label": "Standard Working Hours"
},
{
"default": "0",
"fieldname": "actual_overtime_duration",
"fieldtype": "Time",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Float fieldtype would've been better and easier for calculations

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

This field shows the value as time, making it easier for users to understand. A float value, like 9.4, can be confusing. All other overtime fields display the value in the same format.

"label": "Actual Overtime Duration",
"read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-04-05 20:55:02.905452",
"modified": "2024-10-27 16:33:17.605504",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",
Expand Down
34 changes: 34 additions & 0 deletions hrms/hr/doctype/attendance/attendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
get_holidays_for_employee,
validate_active_employee,
)
from hrms.payroll.doctype.salary_slip.salary_slip import convert_str_time_to_hours


class DuplicateAttendanceError(frappe.ValidationError):
Expand All @@ -43,10 +44,22 @@ def validate(self):
self.validate_overlapping_shift_attendance()
self.validate_employee_status()
self.check_leave_record()
self.validate_overtime_duration()

def on_cancel(self):
self.unlink_attendance_from_checkins()

def validate_overtime_duration(self):
if self.overtime_type:
maximum_overtime_hours = frappe.db.get_value(
"Overtime Type", self.overtime_type, "maximum_overtime_hours_allowed"
)
self.actual_overtime_duration = self.overtime_duration
if maximum_overtime_hours:
iamejaaz marked this conversation as resolved.
Show resolved Hide resolved
overtime_duration_in_hours = convert_str_time_to_hours(self.overtime_duration)
if overtime_duration_in_hours > maximum_overtime_hours:
self.overtime_duration = str(maximum_overtime_hours) + ":00:00"

def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")

Expand Down Expand Up @@ -237,6 +250,27 @@ def unlink_attendance_from_checkins(self):
)


@frappe.whitelist()
def get_shift_type(employee, attendance_date):
ShiftAssignment = frappe.qb.DocType("Shift Assignment")

shift_assignment = (
frappe.qb.from_(ShiftAssignment)
.select(ShiftAssignment.name, ShiftAssignment.shift_type)
.where(ShiftAssignment.docstatus == 1)
.where(ShiftAssignment.employee == employee)
.where(ShiftAssignment.start_date <= attendance_date)
.where((ShiftAssignment.end_date >= attendance_date) | (ShiftAssignment.end_date.isnull()))
.where(ShiftAssignment.status == "Active")
).run(as_dict=1)

if len(shift_assignment):
shift = shift_assignment[0].shift_type
else:
shift = frappe.db.get_value("Employee", employee, "default_shift")
return shift


@frappe.whitelist()
def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond
Expand Down
61 changes: 61 additions & 0 deletions hrms/hr/doctype/employee_checkin/employee_checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# For license information, please see license.txt


from datetime import datetime
from typing import Union

import frappe
from frappe import _
from frappe.model.document import Document
Expand Down Expand Up @@ -202,6 +205,29 @@ def mark_attendance_and_link_log(
try:
frappe.db.savepoint("attendance_creation")
attendance = frappe.new_doc("Attendance")
if shift:
shift_type_overtime = frappe.db.get_value(
doctype="Shift Type",
filters={"name": shift},
fieldname=["overtime_type", "allow_overtime", "start_time", "end_time"],
as_dict=True,
)
if shift_type_overtime.allow_overtime:
standard_working_hours = calculate_time_difference(
shift_type_overtime.start_time, shift_type_overtime.end_time
)
total_working_duration = calculate_time_difference(in_time, out_time)
if total_working_duration > standard_working_hours:
overtime_duration = calculate_time_difference(
standard_working_hours, total_working_duration
)
attendance.update(
{
"overtime_type": shift_type_overtime.overtime_type,
"standard_working_hours": standard_working_hours,
"overtime_duration": overtime_duration,
}
)
attendance.update(
{
"doctype": "Attendance",
Expand Down Expand Up @@ -340,3 +366,38 @@ def update_attendance_in_checkins(log_names: list, attendance_id: str):
.set("attendance", attendance_id)
.where(EmployeeCheckin.name.isin(log_names))
).run()


from datetime import timedelta


def convert_to_timedelta(input_value: str | timedelta) -> timedelta:
"""
Converts a string in the format 'HH:MM:SS'
"""
if isinstance(input_value, str):
# If the input is a string, parse it into a datetime object
time_format = "%H:%M:%S"
time_obj = datetime.strptime(input_value, time_format)
# Convert datetime to timedelta (we only care about the time)
return timedelta(hours=time_obj.hour, minutes=time_obj.minute, seconds=time_obj.second)

return input_value


def calculate_time_difference(start_time, end_time):
"""
Converts inputs to timedelta, finds the difference between start and end times.
"""
# Convert both start and end times to timedelta
start_time_delta = convert_to_timedelta(start_time)
end_time_delta = convert_to_timedelta(end_time)

# Calculate the time difference
time_difference = end_time_delta - start_time_delta

# Return the difference only if it's positive
if time_difference.total_seconds() > 0:
return time_difference
else:
return timedelta(0)
4 changes: 2 additions & 2 deletions hrms/hr/doctype/employee_checkin/test_employee_checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ def make_n_checkins(employee, n, hours_to_reverse=1):
return logs


def make_checkin(employee, time=None, latitude=None, longitude=None):
def make_checkin(employee, time=None, latitude=None, longitude=None, log_type="IN"):
if not time:
time = now_datetime()

Expand All @@ -601,7 +601,7 @@ def make_checkin(employee, time=None, latitude=None, longitude=None):
"employee": employee,
"time": time,
"device_id": "device1",
"log_type": "IN",
"log_type": log_type,
"latitude": latitude,
"longitude": longitude,
}
Expand Down
Empty file.
78 changes: 78 additions & 0 deletions hrms/hr/doctype/overtime_details/overtime_details.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-10-15 16:36:22.056743",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_document_type",
"reference_document",
"date",
"column_break_ilza",
"overtime_type",
"overtime_duration",
"standard_working_hours"
],
"fields": [
{
"fieldname": "reference_document_type",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "reference_document",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document",
"options": "reference_document_type",
"reqd": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "column_break_ilza",
"fieldtype": "Column Break"
},
{
"fieldname": "overtime_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Overtime Type",
"options": "Overtime Type",
"reqd": 1
},
{
"fieldname": "overtime_duration",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Overtime Duration",
"reqd": 1
},
{
"default": "0",
"fieldname": "standard_working_hours",
"fieldtype": "Data",
"label": "Standard Working Hours"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-10-27 19:40:06.521102",
"modified_by": "Administrator",
"module": "HR",
"name": "Overtime Details",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
9 changes: 9 additions & 0 deletions hrms/hr/doctype/overtime_details/overtime_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

# import frappe
from frappe.model.document import Document


class OvertimeDetails(Document):
pass
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-10-13 14:00:52.211875",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"salary_component"
],
"fields": [
{
"fieldname": "salary_component",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Salary Component",
"options": "Salary Component",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-10-13 14:01:49.333952",
"modified_by": "Administrator",
"module": "HR",
"name": "Overtime Salary Component",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

# import frappe
from frappe.model.document import Document


class OvertimeSalaryComponent(Document):
pass
Empty file.
Loading
Loading