Files
ihrm/attendance/methods/utils.py

588 lines
19 KiB
Python

"""
utils.py
This module is used write custom methods
"""
import calendar
from datetime import datetime, time, timedelta
import pandas as pd
from django.core.exceptions import ValidationError
from django.core.paginator import Paginator
from django.db import models
from django.db.models import Q, Sum
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from base.methods import get_pagination
from base.models import WEEK_DAYS, CompanyLeaves, Holidays
from employee.models import Employee
MONTH_MAPPING = {
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12,
}
def format_time(seconds):
"""
this method is used to formate seconds to H:M and return it
args:
seconds : seconds
"""
hour = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int((seconds % 3600) % 60)
return f"{hour:02d}:{minutes:02d}"
def strtime_seconds(time):
"""
this method is used reconvert time in H:M formate string back to seconds and return it
args:
time : time in H:M format
"""
ftr = [3600, 60, 1]
return sum(a * b for a, b in zip(ftr, map(int, time.split(":"))))
def get_diff_obj(first_instance, other_instance, exclude_fields=None):
"""
Compare the fields of two instances and identify the changes.
Args:
first_instance: The first instance to compare.
other_instance: The second instance to compare.
exclude_fields: A list of field names to exclude from comparison (optional).
Returns:
A dictionary of changed fields with their old and new values.
"""
difference = {}
fields_to_compare = first_instance._meta.fields
if exclude_fields:
fields_to_compare = [
field for field in fields_to_compare if field.name not in exclude_fields
]
for field in fields_to_compare:
old_value = getattr(first_instance, field.name)
new_value = getattr(other_instance, field.name)
if old_value != new_value:
difference[field.name] = (old_value, new_value)
return difference
def get_diff_dict(first_dict, other_dict, model=None):
"""
Compare two dictionaries and identify differing key-value pairs.
Args:
first_dict: The first dictionary to compare.
other_dict: The second dictionary to compare.
model: The model class
Returns:
A dictionary of differing keys with their old and new values.
"""
# model is passed as argument if any need of verbose name instead of field name
difference = {}
if model is None:
for key in first_dict:
if first_dict[key] != other_dict[key]:
# get the verbose name of the field
difference[key] = (first_dict[key], other_dict[key])
return difference
for key in first_dict:
if first_dict[key] != other_dict[key]:
# get the verbose name of the field
field = model._meta.get_field(key)
verb_key = field.verbose_name
value = first_dict[key]
other_value = other_dict[key]
if isinstance(field, models.DateField):
if value is not None and value != "None":
value = datetime.strptime(value, "%Y-%m-%d").strftime("%d %b %Y")
if other_value is not None and other_value != "None":
other_value = datetime.strptime(other_value, "%Y-%m-%d").strftime(
"%d %b %Y"
)
elif isinstance(field, models.TimeField):
if value is not None and value != "None":
if len(value.split(":")) == 2:
value = value + ":00"
value = datetime.strptime(value, "%H:%M:%S").strftime("%I:%M %p")
if other_value is not None and value != "None":
if len(other_value.split(":")) == 2:
other_value = other_value + ":00"
if other_value != "None":
other_value = datetime.strptime(
other_value, "%H:%M:%S"
).strftime("%I:%M %p")
else:
other_value = "None"
elif isinstance(field, models.ForeignKey):
if value is not None and len(str(value)):
value = field.related_model.objects.get(id=value)
if other_value is not None and len(str(other_value)):
other_value = field.related_model.objects.get(id=other_value)
difference[verb_key] = (value, other_value)
return difference
def employee_exists(request):
"""
This method return the employee instance and work info if not exists return None instead
"""
employee, employee_work_info = None, None
try:
employee = request.user.employee_get
employee_work_info = employee.employee_work_info
finally:
return (employee, employee_work_info)
def shift_schedule_today(day, shift):
"""
This function is used to find shift schedules for the day,
it will returns min hour,start time seconds end time seconds
args:
shift : shift instance
day : shift day object
"""
schedule_today = day.day_schedule.filter(shift_id=shift)
start_time_sec, end_time_sec, minimum_hour = 0, 0, "00:00"
if schedule_today.exists():
schedule_today = schedule_today[0]
minimum_hour = schedule_today.minimum_working_hour
start_time_sec = strtime_seconds(schedule_today.start_time.strftime("%H:%M"))
end_time_sec = strtime_seconds(schedule_today.end_time.strftime("%H:%M"))
return (minimum_hour, start_time_sec, end_time_sec)
def overtime_calculation(attendance):
"""
This method is used to calculate overtime of the attendance, it will
return difference between attendance worked hour and minimum hour if
and only worked hour greater than minimum hour, else return 00:00
args:
attendance : attendance instance
"""
minimum_hour = attendance.minimum_hour
at_work = attendance.attendance_worked_hour
at_work_sec = strtime_seconds(at_work)
minimum_hour_sec = strtime_seconds(minimum_hour)
if at_work_sec > minimum_hour_sec:
return format_time((at_work_sec - minimum_hour_sec))
return "00:00"
def is_reportingmanger(request, instance):
"""
if the instance have employee id field then you can use this method to know the
request user employee is the reporting manager of the instance
args :
request : request
instance : an object or instance of any model contain employee_id foreign key field
"""
manager = request.user.employee_get
try:
employee_workinfo_manager = (
instance.employee_id.employee_work_info.reporting_manager_id
)
except Exception:
return HttpResponse("This Employee Dont Have any work information")
return manager == employee_workinfo_manager
def validate_hh_mm_ss_format(value):
timeformat = "%H:%M:%S"
try:
validtime = datetime.strptime(value, timeformat)
return validtime.time() # Return the time object if needed
except ValueError as e:
raise ValidationError(_("Invalid format, it should be HH:MM:SS format"))
def validate_time_format(value):
"""
this method is used to validate the format of duration like fields.
"""
if len(value) > 6:
raise ValidationError(_("Invalid format, it should be HH:MM format"))
try:
hour, minute = value.split(":")
if len(hour) > 3 or len(minute) > 2:
raise ValidationError(_("Invalid time"))
hour = int(hour)
minute = int(minute)
if len(str(hour)) > 3 or len(str(minute)) > 2 or minute not in range(60):
raise ValidationError(_("Invalid time, excepted MM:SS"))
except ValueError as error:
raise ValidationError(_("Invalid format")) from error
def attendance_date_validate(date):
"""
Validates if the provided date is not a future date.
:param date: The date to validate.
:raises ValidationError: If the provided date is in the future.
"""
today = datetime.today().date()
if not date:
raise ValidationError(_("Check date format."))
elif date > today:
raise ValidationError(_("You cannot choose a future date."))
def activity_datetime(attendance_activity):
"""
This method is used to convert clock-in and clock-out of activity as datetime object
args:
attendance_activity : attendance activity instance
"""
# in
in_year = attendance_activity.clock_in_date.year
in_month = attendance_activity.clock_in_date.month
in_day = attendance_activity.clock_in_date.day
in_hour = attendance_activity.clock_in.hour
in_minute = attendance_activity.clock_in.minute
# out
out_year = attendance_activity.clock_out_date.year
out_month = attendance_activity.clock_out_date.month
out_day = attendance_activity.clock_out_date.day
out_hour = attendance_activity.clock_out.hour
out_minute = attendance_activity.clock_out.minute
return datetime(in_year, in_month, in_day, in_hour, in_minute), datetime(
out_year, out_month, out_day, out_hour, out_minute
)
def get_week_start_end_dates(week):
"""
This method is use to return the start and end date of the week
"""
# Parse the ISO week date
year, week_number = map(int, week.split("-W"))
# Get the date of the first day of the week
start_date = datetime.strptime(f"{year}-W{week_number}-1", "%Y-W%W-%w").date()
# Calculate the end date by adding 6 days to the start date
end_date = start_date + timedelta(days=6)
return start_date, end_date
def get_month_start_end_dates(year_month):
"""
This method is use to return the start and end date of the month
"""
# split year and month separately
year, month = map(int, year_month.split("-"))
# Get the first day of the month
start_date = datetime(year, month, 1).date()
# Get the last day of the month
_, last_day = calendar.monthrange(year, month)
end_date = datetime(year, month, last_day).date()
return start_date, end_date
def worked_hour_data(labels, records):
"""
To find all the worked hours
"""
data = {
"label": "Worked Hours",
"backgroundColor": "rgba(75, 192, 192, 0.6)",
}
dept_records = []
for dept in labels:
total_sum = records.filter(
employee_id__employee_work_info__department_id__department=dept
).aggregate(total_sum=Sum("hour_account_second"))["total_sum"]
dept_records.append(total_sum / 3600 if total_sum else 0)
data["data"] = dept_records
return data
def pending_hour_data(labels, records):
"""
To find all the pending hours
"""
data = {
"label": "Pending Hours",
"backgroundColor": "rgba(255, 99, 132, 0.6)",
}
dept_records = []
for dept in labels:
total_sum = records.filter(
employee_id__employee_work_info__department_id__department=dept
).aggregate(total_sum=Sum("hour_pending_second"))["total_sum"]
dept_records.append(total_sum / 3600 if total_sum else 0)
data["data"] = dept_records
return data
def get_employee_last_name(attendance):
"""
This method is used to return the last name
"""
if attendance.employee_id.employee_last_name:
return attendance.employee_id.employee_last_name
return ""
def attendance_day_checking(attendance_date, minimum_hour):
# Convert the string to a datetime object
attendance_datetime = datetime.strptime(attendance_date, "%Y-%m-%d")
# Extract name of the day
attendance_day = attendance_datetime.strftime("%A")
# Taking all holidays into a list
leaves = []
holidays = Holidays.objects.all()
for holi in holidays:
start_date = holi.start_date
end_date = holi.end_date
# Convert start_date and end_date to datetime objects
start_date = datetime.strptime(str(start_date), "%Y-%m-%d")
end_date = datetime.strptime(str(end_date), "%Y-%m-%d")
# Add dates in between start date and end date including both
current_date = start_date
while current_date <= end_date:
leaves.append(current_date.strftime("%Y-%m-%d"))
current_date += timedelta(days=1)
# Checking attendance date is in holiday list, if found making the minimum hour to 00:00
for leave in leaves:
if str(leave) == str(attendance_date):
minimum_hour = "00:00"
break
# Making a dictonary contains week day value and leave day pairs
company_leaves = {}
company_leave = CompanyLeaves.objects.all()
for com_leave in company_leave:
a = dict(WEEK_DAYS).get(com_leave.based_on_week_day)
b = com_leave.based_on_week
company_leaves[b] = a
# Checking the attendance date is in which week
week_in_month = str(((attendance_datetime.day - 1) // 7 + 1) - 1)
# Checking the attendance date is in the company leave or not
for pairs in company_leaves.items():
# For all weeks based_on_week is None
if str(pairs[0]) == "None":
if str(pairs[1]) == str(attendance_day):
minimum_hour = "00:00"
break
# Checking with based_on_week and attendance_date week
if str(pairs[0]) == week_in_month:
if str(pairs[1]) == str(attendance_day):
minimum_hour = "00:00"
break
return minimum_hour
def paginator_qry(qryset, page_number):
"""
This method is used to paginate queryset
"""
paginator = Paginator(qryset, get_pagination())
qryset = paginator.get_page(page_number)
return qryset
def monthly_leave_days(month, year):
leave_dates = []
holidays = Holidays.objects.filter(start_date__month=month, start_date__year=year)
leave_dates += list(holidays.values_list("start_date", flat=True))
company_leaves = CompanyLeaves.objects.all()
for company_leave in company_leaves:
year = year
month = month
based_on_week = company_leave.based_on_week
based_on_week_day = company_leave.based_on_week_day
if based_on_week != None:
calendar.setfirstweekday(6)
month_calendar = calendar.monthcalendar(year, month)
weeks = month_calendar[int(based_on_week)]
weekdays_in_weeks = [day for day in weeks if day != 0]
for day in weekdays_in_weeks:
date_name = datetime.strptime(
f"{year}-{month:02}-{day:02}", "%Y-%m-%d"
).date()
if (
date_name.weekday() == int(based_on_week_day)
and date_name not in leave_dates
):
leave_dates.append(date_name)
else:
calendar.setfirstweekday(0)
month_calendar = calendar.monthcalendar(year, month)
for week in month_calendar:
if week[int(based_on_week_day)] != 0:
date_name = datetime.strptime(
f"{year}-{month:02}-{week[int(based_on_week_day)]:02}",
"%Y-%m-%d",
).date()
if date_name not in leave_dates:
leave_dates.append(date_name)
return leave_dates
def validate_time_in_minutes(value):
"""
this method is used to validate the format of duration like fields.
"""
if len(value) > 5:
raise ValidationError(_("Invalid format, it should be MM:SS format"))
try:
minutes, sec = value.split(":")
if len(minutes) > 2 or len(sec) > 2:
raise ValidationError(_("Invalid time, excepted MM:SS"))
minutes = int(minutes)
sec = int(sec)
if minutes not in range(60) or sec not in range(60):
raise ValidationError(_("Invalid time, excepted MM:SS"))
except ValueError as e:
raise ValidationError(_("Invalid format, excepted MM:SS")) from e
class Request:
"""
Represents a request for clock-in or clock-out.
Attributes:
- user: The user associated with the request.
- date: The date of the request.
- time: The time of the request.
- path: The path associated with the request (default: "/").
- session: The session data associated with the request (default: {"title": None}).
"""
def __init__(
self,
user,
date,
time,
datetime,
) -> None:
self.user = user
self.path = "/"
self.session = {"title": None}
self.date = date
self.time = time
self.datetime = datetime
self.META = META()
class META:
"""
Provides access to HTTP metadata keys.
"""
@classmethod
def keys(cls):
"""
Retrieve the list of available HTTP metadata keys.
Returns:
list: A list of HTTP metadata keys.
"""
return ["HTTP_HX_REQUEST"]
def parse_time(time_str):
time_formats = {
"hh:mm A": "%I:%M %p", # 12-hour format
"HH:mm": "%H:%M", # 24-hour format
}
if isinstance(time_str, time): # Check if it's already a time object
return time_str
if isinstance(time_str, str):
for format_str in time_formats.values():
try:
return datetime.strptime(time_str, format_str).time()
except ValueError:
continue
return None
def parse_date(date_str, error_key, activity):
try:
return pd.to_datetime(date_str).date()
except (pd.errors.ParserError, ValueError):
activity[error_key] = f"Invalid date format for {error_key.split()[-1]}"
return None
def get_date(date):
date_formats = {
"DD-MM-YYYY": "%d-%m-%Y",
"DD.MM.YYYY": "%d.%m.%Y",
"DD/MM/YYYY": "%d/%m/%Y",
"MM/DD/YYYY": "%m/%d/%Y",
"YYYY-MM-DD": "%Y-%m-%d",
"YYYY/MM/DD": "%Y/%m/%d",
"MMMM D, YYYY": "%B %d, %Y",
"DD MMMM, YYYY": "%d %B, %Y",
"MMM. D, YYYY": "%b. %d, %Y",
"D MMM. YYYY": "%d %b. %Y",
"dddd, MMMM D, YYYY": "%A, %B %d, %Y",
}
if isinstance(date, datetime):
return date
elif isinstance(date, str):
for format_name, format_str in date_formats.items():
try:
return datetime.strptime(date, format_str)
except ValueError:
continue
return None
def sort_activity_dicts(activity_dicts):
for activity in activity_dicts:
activity["Attendance Date"] = get_date(activity["Attendance Date"])
# Filter out any entries where the date could not be parsed
activity_dicts = [
activity
for activity in activity_dicts
if activity["Attendance Date"] is not None
]
sorted_activity_dicts = sorted(activity_dicts, key=lambda x: x["Attendance Date"])
return sorted_activity_dicts