[ADD] EMPLOYEE/ATTENDANCE: Late come/ early out penalty implementation

This commit is contained in:
Horilla
2024-01-06 09:54:16 +05:30
parent c6f27d5c01
commit 6aacc04dd9
14 changed files with 395 additions and 118 deletions

View File

@@ -10,6 +10,7 @@ from .models import (
AttendanceOverTime,
AttendanceLateComeEarlyOut,
AttendanceValidationCondition,
PenaltyAccount,
)
# Register your models here.
@@ -18,3 +19,4 @@ admin.site.register(AttendanceActivity)
admin.site.register(AttendanceOverTime)
admin.site.register(AttendanceLateComeEarlyOut)
admin.site.register(AttendanceValidationCondition)
admin.site.register(PenaltyAccount)

View File

@@ -17,6 +17,7 @@ from attendance.models import (
AttendanceOverTime,
AttendanceLateComeEarlyOut,
AttendanceActivity,
PenaltyAccount,
strtime_seconds,
)
from base.filters import FilterSet
@@ -242,6 +243,16 @@ class LateComeEarlyOutFilter(FilterSet):
self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}"
class PenaltyFilter(FilterSet):
"""
PenaltyFilter
"""
class Meta:
model = PenaltyAccount
fields = "__all__"
class AttendanceActivityFilter(FilterSet):
"""
Filter set class for AttendanceActivity model
@@ -332,6 +343,7 @@ class AttendanceFilters(FilterSet):
Args:
FilterSet (class): custom filter set class to apply styling
"""
id = django_filters.NumberFilter(field_name="id")
search = django_filters.CharFilter(method=filter_by_name)
employee = django_filters.CharFilter(field_name="employee_id__id")

View File

@@ -42,6 +42,7 @@ from attendance.models import (
AttendanceActivity,
AttendanceLateComeEarlyOut,
AttendanceValidationCondition,
PenaltyAccount,
strtime_seconds,
)
from django.utils.html import format_html
@@ -683,6 +684,17 @@ class AttendanceExportForm(forms.Form):
)
class PenaltyAccountForm(ModelForm):
"""
PenaltyAccountForm
"""
class Meta:
model = PenaltyAccount
fields = "__all__"
exclude = ["late_early_id"]
class LateComeEarlyOutExportForm(forms.Form):
model_fields = AttendanceLateComeEarlyOut._meta.get_fields()
field_choices_1 = [

View File

@@ -12,12 +12,21 @@ from datetime import datetime, date, timedelta
from django.db import models
from django.db.models import Q
from django.core.exceptions import ValidationError
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_save
import pandas as pd
from base.models import Company, EmployeeShift, EmployeeShiftDay, WorkType
from base.horilla_company_manager import HorillaCompanyManager
from employee.models import Employee
from leave.models import WEEK_DAYS, WEEKS, CompanyLeave, Holiday, LeaveRequest
from leave.models import (
WEEK_DAYS,
WEEKS,
CompanyLeave,
Holiday,
LeaveRequest,
LeaveType,
)
from attendance.methods.differentiate import get_diff_dict
# Create your models here.
@@ -256,7 +265,7 @@ class Attendance(models.Model):
keys = diffs.keys()
return keys
def get_last_clock_out(self,null_activity=False):
def get_last_clock_out(self, null_activity=False):
"""
This method is used to get the last attendance activity if exists
"""
@@ -712,6 +721,13 @@ class AttendanceLateComeEarlyOut(models.Model):
objects = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
created_at = models.DateTimeField(auto_now_add=True,null=True)
def get_penalties_count(self):
"""
This method is used to return the total penalties in the late early instance
"""
return self.penaltyaccount_set.count()
def save(self, *args, **kwargs) -> None:
super().save(*args, **kwargs)
@@ -754,3 +770,96 @@ class AttendanceValidationCondition(models.Model):
super().clean()
if not self.id and AttendanceValidationCondition.objects.exists():
raise ValidationError(_("You cannot add more conditions."))
months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
class PenaltyAccount(models.Model):
"""
LateComeEarlyOutPenaltyAccount
"""
employee_id = models.ForeignKey(
Employee,
on_delete=models.PROTECT,
related_name="penalty_set",
editable=False,
verbose_name="Employee",
null=True,
)
late_early_id = models.ForeignKey(
AttendanceLateComeEarlyOut,
on_delete=models.CASCADE,
null=True,
)
leave_type_id = models.ForeignKey(
LeaveType,
on_delete=models.DO_NOTHING,
blank=True,
null=True,
verbose_name="Leave type",
)
minus_leaves = models.FloatField(default=0.0, null=True)
deduct_from_carry_forward = models.BooleanField(default=False)
penalty_amount = models.FloatField(default=0.0, null=True)
created_at = models.DateTimeField(auto_now_add=True,null=True)
def clean(self) -> None:
super().clean()
if (
self.minus_leaves or self.deduct_from_carry_forward
) and not self.leave_type_id:
raise ValidationError({"leave_type_id": "Leave type is required"})
return
class Meta:
ordering = ["-created_at"]
@receiver(post_save, sender=PenaltyAccount)
def create_initial_stage(sender, instance, created, **kwargs):
"""
This is post save method, used to create initial stage for the recruitment
"""
if created:
penalty_amount = instance.penalty_amount
if penalty_amount:
from payroll.models.models import Deduction
penalty = Deduction()
penalty.title = f"{instance.late_early_id.get_type_display()} penalty"
penalty.one_time_date = instance.late_early_id.attendance_id.attendance_date
penalty.include_active_employees = False
penalty.is_fixed = True
penalty.amount = instance.penalty_amount
penalty.only_show_under_employee = True
penalty.save()
penalty.specific_employees.add(instance.employee_id)
if instance.leave_type_id and instance.minus_leaves:
available = instance.employee_id.available_leave.filter(
leave_type_id=instance.leave_type_id
).first()
unit = round(instance.minus_leaves * 2) / 2
if not instance.deduct_from_carry_forward:
available.available_days = max(0, (available.total_leave_days - unit))
else:
available.carryforward_days = max(
0, (available.carryforward_days - unit)
)
available.save()

View File

@@ -1,85 +1,45 @@
{% extends 'settings.html' %} {% load i18n %} {% block settings %}
<div class="oh-inner-sidebar-content mb-4">
<div class="oh-inner-sidebar-content__header d-flex justify-content-between align-items-center">
<h2 class="oh-inner-sidebar-content__title">{% trans "Employee Type" %}</h2>
{% if not condition %}
<button
class="oh-btn oh-btn--secondary oh-btn--shadow"
hx-target="#updateForm"
type="button"
hx-get="{% url 'attendance-settings-create'%}"
class="oh-btn oh-btn--info"
data-toggle="oh-modal-toggle"
data-target="#updateModal"
>
<ion-icon name="add-outline" class="me-1"></ion-icon>
{% trans "Create" %}
<div class="oh-inner-sidebar-content">
<div class="oh-inner-sidebar-content__header d-flex justify-content-between align-items-center">
<h2 class="oh-inner-sidebar-content__title">{% trans 'Condition' %}</h2>
{% if not condition %}
<button class="oh-btn oh-btn--secondary oh-btn--shadow" hx-target="#updateForm" type="button" hx-get="{% url 'attendance-settings-create' %}" class="oh-btn oh-btn--info" data-toggle="oh-modal-toggle" data-target="#updateModal">
<ion-icon name="add-outline" class="me-1"></ion-icon>
{% trans 'Create' %}
</button>
{% endif %}
{% endif %}
</div>
{% if condition %}
<div class="oh-sticky-table">
<div class="oh-sticky-table__table oh-table--sortable">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">
{% trans "Auto Validate Till" %}
</div>
<div class="oh-sticky-table__th">
{% trans "Min Hour To Approve OT" %}
</div>
<div class="oh-sticky-table__th">{% trans "OT Cut-Off/Day" %}</div>
<div class="oh-sticky-table__th"></div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% if condition != None %}
<div class="oh-sticky-table__tr" draggable="true">
<div class="oh-sticky-table__td">
{{condition.validation_at_work}}
</div>
<div class="oh-sticky-table__td">
{{condition.minimum_overtime_to_approve}}
</div>
<div class="oh-sticky-table__td">{{condition.overtime_cutoff}}</div>
<div class="oh-sticky-table__td">
<a
hx-get="{% url 'attendance-settings-update' condition.id %}"
hx-target="#updateForm"
type="button"
class="oh-btn oh-btn--info"
data-toggle="oh-modal-toggle"
data-target="#updateModal"
>
{% trans "Edit" %}</a
>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif%}
</div>
<div
class="oh-modal"
id="updateModal"
role="dialog"
aria-labelledby="updateModal"
aria-hidden="true"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<button class="oh-modal__close" aria-label="Close">
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="updateForm"></div>
</div>
</div>
{% endblock settings %}
{% if condition %}
<div class="oh-sticky-table">
<div class="oh-sticky-table__table oh-table--sortable">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">
{% trans 'Auto Validate Till' %}
</div>
<div class="oh-sticky-table__th">
{% trans 'Min Hour To Approve OT' %}
</div>
<div class="oh-sticky-table__th">
{% trans 'OT Cut-Off/Day' %}
</div>
<div class="oh-sticky-table__th"></div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% if condition != None %}
<div class="oh-sticky-table__tr" draggable="true">
<div class="oh-sticky-table__td">{{ condition.validation_at_work }}</div>
<div class="oh-sticky-table__td">{{ condition.minimum_overtime_to_approve }}</div>
<div class="oh-sticky-table__td">{{ condition.overtime_cutoff }}</div>
<div class="oh-sticky-table__td">
<a hx-get="{% url 'attendance-settings-update' condition.id %}" hx-target="#updateForm" type="button" class="oh-btn oh-btn--info" data-toggle="oh-modal-toggle" data-target="#updateModal">{% trans 'Edit' %}</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% load i18n %}
{% include 'filter_tags.html' %}
{% load basefilters %}
<div div class="oh-sticky-table" >
<div class="oh-sticky-table__table oh-table--sortable">
<div class="oh-sticky-table__thead">
@@ -18,6 +19,8 @@
<div class='oh-sticky-table__th' scope="col" hx-target='#report-container' hx-get="{% url 'late-come-early-out-search' %}?{{pd}}&sortby=attendance_id__attendance_clock_out_date">{% trans "Out Date" %}</div>
<div class='oh-sticky-table__th' scope="col" hx-target='#report-container' hx-get="{% url 'late-come-early-out-search' %}?{{pd}}&sortby=">{% trans "Min Hour" %}</div>
<div class='oh-sticky-table__th' scope="col" hx-target='#report-container' hx-get="{% url 'late-come-early-out-search' %}?{{pd}}&sortby=attendance_id__at_work_second">{% trans "At Work" %}</div>
<div class='oh-sticky-table__th' scope="col"></div>
<div class='oh-sticky-table__th' scope="col" style="width: 150px;"></div>
<div class='oh-sticky-table__th' scope="col" style="width: 80px;"></div>
</div>
</div>
@@ -54,12 +57,28 @@
<div class='oh-sticky-table__td timeformat_changer'>{{late_in_early_out.attendance_id.attendance_clock_in}}</div>
<div class='oh-sticky-table__td dateformat_changer'>{{late_in_early_out.attendance_id.attendance_clock_in_date}}</div>
<div class='oh-sticky-table__td timeformat_changer'>{{late_in_early_out.attendance_id.attendance_clock_out}}</div>
<div class='oh-sticky-table__td dateformat_changer'>{{late_in_early_out.attendance_id.attendance_clock_out_date}}</div>
<div class='oh-sticky-table__td dateformat_changer'>{{laXte_in_early_out.attendance_id.attendance_clock_out_date}}</div>
<div class='oh-sticky-table__td'>{{late_in_early_out.attendance_id.minimum_hour}}</div>
<div class='oh-sticky-table__td'>{{late_in_early_out.attendance_id.attendance_worked_hour}}</div>
<div class='oh-sticky-table__td'>
{% if late_in_early_out.get_penalties_count %}
<div class="" data-target="#penaltyViewModal" data-toggle="oh-modal-toggle" hx-get="{% url "view-penalties" %}?late_early_id={{late_in_early_out.id}}" hx-target="#penaltyViewModalBody" align="center" style="background-color: rgba(229, 79, 56, 0.036); border: 2px solid rgb(229, 79, 56); border-radius: 18px; padding: 10px; font-weight: bold; width: 85%;">Penalties :{{late_in_early_out.get_penalties_count}}</div>
{% endif %}
</div>
<div class="oh-sticky-table__td">
<div class="oh-btn-group">
{% if request.user|is_reportingmanager or perms.attendance.chanage_penaltyaccount %}
<button
data-toggle="oh-modal-toggle"
data-target="#penaltyModal"
hx-target="#penaltyModalBody"
hx-get="{% url "cut-penalty" late_in_early_out.id %}"
type='submit'
class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
title="{% trans 'Penalty' %}">
<ion-icon name="information-circle-outline"></ion-icon>
</button>
{% endif %}
{% if perms.attendance.delete_attendancelatecomeearlyout %}
<form action="{% url 'late-come-early-out-delete' late_in_early_out.id %}"
onclick="event.stopPropagation()" onsubmit="return confirm('{% trans "Are you sure want to delete this attendance?" %}')"

View File

@@ -2,25 +2,42 @@
{% load static %}
{% load i18n %}
{% block content %}
{% include 'attendance/late_come_early_out/nav.html' %}
<div class="oh-checkpoint-badge mb-2" id="selectedLatecome" data-ids="[]" data-clicked="" style="display:none;" >
{% trans "Selected Attendance" %}
</div>
<div class="oh-wrapper">
{% include 'attendance/late_come_early_out/nav.html' %}
<div class="oh-checkpoint-badge mb-2" id="selectedLatecome" data-ids="[]" data-clicked="" style="display:none;">
{% trans 'Selected Attendance' %}
</div>
<div class="oh-wrapper">
<div class="oh-checkpoint-badge text-success mb-2" id="selectAllLatecome" style="cursor: pointer;">
{% trans "Select All Attendance" %}
{% trans 'Select All Attendance' %}
</div>
<div class="oh-checkpoint-badge text-secondary mb-2" id="unselectAllLatecome" style="cursor: pointer;">
{% trans "Unselect All Attendance" %}
{% trans 'Unselect All Attendance' %}
</div>
<div class="oh-checkpoint-badge text-info mb-2" id="exportLatecome" style="cursor: pointer;">
{% trans "Export Attendance" %}
{% trans 'Export Attendance' %}
</div>
<div class="oh-checkpoint-badge text-danger mb-2" id="selectedShowLatecome" >
<div class="oh-checkpoint-badge text-danger mb-2" id="selectedShowLatecome"></div>
<div id="report-container">
{% include 'attendance/late_come_early_out/report_list.html' %}
</div>
<div id='report-container'>
{% include 'attendance/late_come_early_out/report_list.html' %}
</div>
<div class="oh-modal" id="penaltyModal" role="dialog" aria-hidden="true">
<div class="oh-modal__dialog" style="max-width: 550px">
<div class="oh-modal__dialog-header">
<button type="button" class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
</div>
<div class="oh-modal__dialog-body" id="penaltyModalBody"></div>
</div>
</div>
</div>
<div class="oh-modal" id="penaltyViewModal" role="dialog" aria-hidden="true">
<div class="oh-modal__dialog" style="max-width: 1050px">
<div class="oh-modal__dialog-header">
<button type="button" class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
</div>
<div class="oh-modal__dialog-body" id="penaltyViewModalBody"></div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
<div class="oh-timeoff-modal__profile-content">
<div class="oh-profile mb-2">
<div class="oh-profile__avatar">
<img src="{{ instance.employee_id.get_avatar }}" class="oh-profile__image me-2" alt="Mary Magdalene" />
</div>
<div class="oh-timeoff-modal__profile-info">
<span class="oh-timeoff-modal__user fw-bold">{{ instance.employee_id.get_full_name }}</span>
<span class="oh-timeoff-modal__user m-0" style="font-size: 18px; color: #4d4a4a">
{{ instance.employee_id.get_department }} /
{{ instance.employee_id.get_job_position }}
</span>
</div>
</div>
</div>
<form hx-post="{% url 'cut-penalty' instance.id %}" hx-target="#penaltyModalBody">
{{ form.as_p }}
<div class="oh-sticky-table__table oh-table--sortable">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">Leave Type</div>
<div class="oh-sticky-table__th">Available Days</div>
<div class="oh-sticky-table__th">Carry Forward Days</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for acc in available %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{{ acc.leave_type_id }}</div>
<div class="oh-sticky-table__th">{{ acc.available_days }}</div>
<div class="oh-sticky-table__th">{{ acc.carryforward_days }}</div>
</div>
{% endfor %}
</div>
</div>
<ol class="mt-3">
<li>
<i>Leave type is optional when 'minus leave' is 0</i>
</li>
<li>
<i>Penalty amount will affect payslip on the date</i>
</li>
<li>
<i>By default minus leave will cut/deduct from available leaves</i>
</li>
<li>
<i>By enabling 'Deduct from carry forward' leave will cut/deduct from carry forward days</i>
</li>
</ol>
<div class="d-flex flex-row-reverse">
<button type="submit" class="oh-btn oh-btn--secondary mt-2 mr-0 pl-4 pr-5 oh-btn--w-100-resp">Save</button>
</div>
</form>

View File

@@ -0,0 +1,30 @@
<div class="oh-sticky-table__table mt-3">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">Leave Type</div>
<div class="oh-sticky-table__th">Minus Days</div>
<div class="oh-sticky-table__th">
Deducted From <span title="Carry Forward Days">CFD</span>
</div>
<div class="oh-sticky-table__th">Penalty amount</div>
<div class="oh-sticky-table__th">Created Date</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for acc in records %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__td">{{ acc.leave_type_id }}</div>
<div class="oh-sticky-table__td">{{ acc.minus_leaves }}</div>
<div class="oh-sticky-table__td">
{% if acc.deduct_from_carry_forward %}
Yes
{% else %}
No
{% endif %}
</div>
<div class="oh-sticky-table__td">{{ acc.penalty_amount }}</div>
<div class="oh-sticky-table__td">{{ acc.created_at }}</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -8,6 +8,7 @@ from django.urls import path
import attendance.views.clock_in_out
import attendance.views.dashboard
import attendance.views.penalty
import attendance.views.search
import attendance.views.requests
from .views import views
@@ -286,5 +287,11 @@ urlpatterns = [
views.latecome_attendance_select_filter,
name="latecome-attendance-select-filter",
),
path("pending-hours/",attendance.views.dashboard.pending_hours,name="pending-hours")
path(
"pending-hours/", attendance.views.dashboard.pending_hours, name="pending-hours"
),
path(
"cut-penalty/<int:instance_id>/", attendance.views.penalty.cut_available_leave, name="cut-penalty"
),
path("view-penalties",attendance.views.penalty.view_penalties,name="view-penalties")
]

View File

@@ -0,0 +1,56 @@
"""
attendance/views/penalty.py
This module is used to write late come early out penatly methods
"""
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.contrib import messages
from attendance.forms import PenaltyAccountForm
from attendance.filters import PenaltyFilter
from attendance.models import AttendanceLateComeEarlyOut, PenaltyAccount
from leave.models import AvailableLeave
from employee.models import Employee
from horilla.decorators import login_required, manager_can_enter
@login_required
@manager_can_enter("leave.change_availableleave")
def cut_available_leave(request, instance_id):
"""
This method is used to create the penalties
"""
instance = AttendanceLateComeEarlyOut.objects.get(id=instance_id)
form = PenaltyAccountForm()
available = AvailableLeave.objects.filter(employee_id=instance.employee_id)
if request.method == "POST":
form = PenaltyAccountForm(request.POST)
if form.is_valid():
penalty_instance = form.instance
penalty = PenaltyAccount()
penalty.late_early_id = instance
penalty.employee_id = instance.employee_id
penalty.leave_type_id = penalty_instance.leave_type_id
penalty.minus_leaves = penalty_instance.minus_leaves
penalty.penalty_amount = penalty_instance.penalty_amount
penalty.save()
messages.success(request, "Penalty/Fine added")
return HttpResponse("<script>window.location.reload()</script>")
return render(
request,
"attendance/penalty/form.html",
{"available": available, "form": form, "instance": instance},
)
@login_required
@manager_can_enter("attendance.view_penalty")
def view_penalties(request):
"""
This method is used to filter or view the penalties
"""
records = PenaltyFilter(request.GET).qs
print("-------------")
print(records)
print("-------------")
return render(request, "attendance/penalty/penalty_view.html", {"records": records})

View File

@@ -180,24 +180,16 @@
>{% trans "Allowance & Deduction" %}</a
>
</li>
{% comment %} <li class="oh-general__tab">
<a
href="#"
class="oh-general__tab-link"
data-action="general-tab"
data-target="#team"
>{% trans "Team" %}</a
>
</li>
<li class="oh-general__tab">
<a
href="#"
class="oh-general__tab-link"
data-action="general-tab"
data-target="#timesheet"
>{% trans "Timesheet" %}</a
data-target="#penaltyTarget"
id="penalty"
class="oh-general__tab-link"
role="button"
>{% trans "Penalty Account" %}</a
>
</li> {% endcomment %}
</li>
<li class="oh-general__tab">
<a
hx-get={% url 'profile-asset-tab' employee.id %}
@@ -229,12 +221,17 @@
id="personal_target"
>
</div>
<div
class="oh-general__tab-target oh-profile__info-tab mb-4 d-none"
id="payroll"
class="oh-general__tab-target oh-profile__info-tab mb-4"
id="penaltyTarget"
>
{% include 'tabs/payroll-tab.html' %}
{% include 'tabs/penalty_account.html' %}
</div>
<div
class="oh-general__tab-target oh-profile__info-tab mb-4 d-none"
id="payroll"
>
{% include 'tabs/payroll-tab.html' %}
</div>
<div
class="oh-general__tab-target oh-profile__info-tab mb-4 d-none"

View File

@@ -3,6 +3,7 @@
{% load basefilters %}
{% comment %} {% include 'employee_nav.html' %} {% endcomment %}
<style>
.enlarge-image-container {
display: none;

View File

@@ -0,0 +1,2 @@
{% load i18n %}
<div hx-get="{% url "view-penalties" %}?employee_id={{employee.id}}" hx-trigger="load"></div>