[ADD] OFFBOARDING: App to manage offboarding procedures of employees from the company

This commit is contained in:
Horilla
2024-01-23 15:21:53 +05:30
parent 66d6612b26
commit 5da35d196c
26 changed files with 1412 additions and 0 deletions

0
offboarding/__init__.py Normal file
View File

13
offboarding/admin.py Normal file
View File

@@ -0,0 +1,13 @@
from django.contrib import admin
from offboarding.models import (
OffboardingStageMultipleFile,
OffboardingNote,
OffboardingTask,
EmployeeTask,
)
# Register your models here.
admin.site.register(
[OffboardingStageMultipleFile, OffboardingNote, OffboardingTask, EmployeeTask]
)

6
offboarding/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class OffboardingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'offboarding'

59
offboarding/decorators.py Normal file
View File

@@ -0,0 +1,59 @@
"""
offboarding/decorators.py
This module is used to write custom authentication decorators for offboarding module
"""
from django.contrib import messages
from django.http import HttpResponseRedirect
from horilla.decorators import decorator_with_arguments
from offboarding.models import Offboarding, OffboardingStage, OffboardingTask
@decorator_with_arguments
def any_manager_can_enter(function, perm):
def _function(request, *args, **kwargs):
employee = request.user.employee_get
if request.user.has_perm(perm) or (
Offboarding.objects.filter(managers=employee).exists()
| OffboardingStage.objects.filter(managers=employee).exists()
| OffboardingTask.objects.filter(managers=employee).exists()
):
return function(request, *args, **kwargs)
else:
messages.info(request, "You dont have permission.")
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return _function
@decorator_with_arguments
def offboarding_manager_can_enter(function, perm):
def _function(request, *args, **kwargs):
employee = request.user.has_perm(perm)
if (
request.user.has_perm(perm)
or Offboarding.objects.filter(managers=employee).exists()
):
return function(request, *args, **kwargs)
else:
messages.info(request, "You dont have permission.")
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return _function
@decorator_with_arguments
def offboarding_or_stage_manager_can_enter(function, perm):
def _function(request, *args, **kwargs):
employee = request.user.has_perm(perm)
if (
request.user.has_perm(perm)
or Offboarding.objects.filter(managers=employee).exists()
or OffboardingStage.objects.filter(managers=employee).exists()
):
return function(request, *args, **kwargs)
else:
messages.info(request, "You dont have permission.")
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return _function

200
offboarding/forms.py Normal file
View File

@@ -0,0 +1,200 @@
"""
offboarding/forms.py
This module is used to register forms for offboarding app
"""
from typing import Any
from django import forms
from django.template.loader import render_to_string
from base.forms import ModelForm
from employee.forms import MultipleFileField
from offboarding.models import (
EmployeeTask,
Offboarding,
OffboardingEmployee,
OffboardingNote,
OffboardingStage,
OffboardingStageMultipleFile,
OffboardingTask,
)
class OffboardingForm(ModelForm):
"""
OffboardingForm model form class
"""
verbose_name = "Offboarding"
class Meta:
model = Offboarding
fields = "__all__"
def as_p(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
context = {"form": self}
table_html = render_to_string("common_form.html", context)
return table_html
class OffboardingStageForm(ModelForm):
"""
OffboardingStage model form
"""
verbose_name = "Stage"
class Meta:
model = OffboardingStage
fields = "__all__"
exclude = [
"offboarding_id",
]
def as_p(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
context = {"form": self}
table_html = render_to_string("common_form.html", context)
return table_html
class OffboardingEmployeeForm(ModelForm):
"""
OffboardingEmployeeForm model form
"""
verbose_name = "Offboarding "
class Meta:
model = OffboardingEmployee
fields = "__all__"
widgets = {
"notice_period_starts": forms.DateTimeInput(attrs={"type": "date"}),
"notice_period_ends": forms.DateTimeInput(attrs={"type": "date"}),
}
def as_p(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
context = {"form": self}
table_html = render_to_string("common_form.html", context)
return table_html
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.initial["notice_period_starts"] = self.instance.notice_period_starts.strftime("%Y-%m-%d")
self.initial["notice_period_ends"] = self.instance.notice_period_ends.strftime("%Y-%m-%d")
class StageSelectForm(ModelForm):
"""
This form is used to register drop down for the pipeline
"""
class Meta:
model = OffboardingEmployee
fields = [
"stage_id",
]
def __init__(self, *args, offboarding=None, **kwargs):
super().__init__(*args, **kwargs)
attrs = self.fields["stage_id"].widget.attrs
attrs["onchange"] = "offboardingUpdateStage($(this))"
attrs["class"] = "w-100 oh-select-custom"
self.fields["stage_id"].widget.attrs.update(attrs)
self.fields["stage_id"].empty_label = None
self.fields["stage_id"].queryset = OffboardingStage.objects.filter(
offboarding_id=offboarding
)
self.fields["stage_id"].label = ""
class NoteForm(ModelForm):
"""
Offboarding note model form
"""
verbose_name = "Add Note"
class Meta:
model = OffboardingNote
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attachment"] = MultipleFileField(label="Attachements")
self.fields["attachment"].required = False
def as_p(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
context = {"form": self}
table_html = render_to_string("common_form.html", context)
return table_html
def save(self, commit: bool = ...) -> Any:
multiple_attachment_ids = []
attachemnts = None
if self.files.getlist("attachment"):
attachemnts = self.files.getlist("attachment")
self.instance.attachemnt = attachemnts[0]
multiple_attachment_ids = []
for attachemnt in attachemnts:
file_instance = OffboardingStageMultipleFile()
file_instance.attachment = attachemnt
file_instance.save()
multiple_attachment_ids.append(file_instance.pk)
instance = super().save(commit)
if commit:
instance.attachments.add(*multiple_attachment_ids)
return instance, attachemnts
class TaskForm(ModelForm):
"""
TaskForm model form
"""
verbose_name = "Offboarding Task"
tasks_to = forms.ModelMultipleChoiceField(
queryset=OffboardingEmployee.objects.all()
)
class Meta:
model = OffboardingTask
fields = "__all__"
exclude = [
"status",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["stage_id"].empty_label = "All Stages in Offboarding"
self.fields["managers"].empty_label = None
def as_p(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
context = {"form": self}
table_html = render_to_string("common_form.html", context)
return table_html
def save(self, commit: bool = ...) -> Any:
super().save(commit)
if commit:
employees = self.cleaned_data["tasks_to"]
print(employees)
for employee in employees:
assinged_task = EmployeeTask.objects.get_or_create(
employee_id=employee,
task_id=self.instance,
)

View File

199
offboarding/models.py Normal file
View File

@@ -0,0 +1,199 @@
from collections.abc import Iterable
from typing import Any
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from base import thread_local_middleware
from employee.models import Employee
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
# Create your models here.
class Offboarding(models.Model):
"""
Offboarding model
"""
statuses = [("ongoing", "Ongoing"), ("completed", "Completed")]
title = models.CharField(max_length=20)
description = models.TextField()
managers = models.ManyToManyField(Employee)
created_at = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=10, default="ongoing", choices=statuses)
is_active = models.BooleanField(default=True)
class OffboardingStage(models.Model):
"""
Offboarding model
"""
types = [
("notice_period", "Notice period"),
("fnf", "FnF Settlement"),
("other", "Other"),
("archived", "Archived"),
]
title = models.CharField(max_length=20)
type = models.CharField(max_length=13, choices=types)
offboarding_id = models.ForeignKey(Offboarding, on_delete=models.PROTECT)
managers = models.ManyToManyField(Employee)
sequence = models.IntegerField(default=0, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return str(self.title)
def is_archived_stage(self):
"""
This method is to check the stage is archived or not
"""
return self.type == "archived"
@receiver(post_save, sender=Offboarding)
def create_initial_stage(sender, instance, created, **kwargs):
"""
This is post save method, used to create initial stage for the recruitment
"""
if created:
initial_stage = OffboardingStage()
initial_stage.title = "Notice Period"
initial_stage.offboarding_id = instance
initial_stage.type = "notice_period"
initial_stage.save()
class OffboardingStageMultipleFile(models.Model):
"""
OffboardingStageMultipleFile
"""
attachment = models.FileField(upload_to="offboarding/attachments")
created_at = models.DateTimeField(auto_now_add=True)
class OffboardingEmployee(models.Model):
"""
OffboardingEmployee model / Employee on stage
"""
units = [("day", "days"), ("month", "Month")]
employee_id = models.OneToOneField(
Employee, on_delete=models.CASCADE, verbose_name="Employee"
)
stage_id = models.ForeignKey(
OffboardingStage, on_delete=models.PROTECT, verbose_name="Stage"
)
notice_period = models.IntegerField()
unit = models.CharField(max_length=10, choices=units)
notice_period_starts = models.DateField()
notice_period_ends = models.DateField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.employee_id.get_full_name()
class OffboardingTask(models.Model):
"""
OffboardingTask model
"""
title = models.CharField(max_length=30)
managers = models.ManyToManyField(Employee)
stage_id = models.ForeignKey(
OffboardingStage,
on_delete=models.PROTECT,
verbose_name="Stage",
null=True,
blank=True,
)
class Meta:
unique_together = ["title", "stage_id"]
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.title
class EmployeeTask(models.Model):
"""
EmployeeTask model
"""
statuses = [
("todo", "Todo"),
("inprogress", "Inprogress"),
("stuck", "Stuck"),
("completed", "Completed"),
]
employee_id = models.ForeignKey(
OffboardingEmployee,
on_delete=models.CASCADE,
verbose_name="Employee",
null=True,
)
status = models.CharField(max_length=10, choices=statuses, default="todo")
task_id = models.ForeignKey(OffboardingTask, on_delete=models.CASCADE)
history = HorillaAuditLog(
related_name="history_set",
bases=[
HorillaAuditInfo,
],
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ["employee_id", "task_id"]
class ExitReason(models.Model):
"""
ExitReason model
"""
title = models.CharField(max_length=50)
description = models.TextField()
offboarding_employee_id = models.ForeignKey(
OffboardingEmployee, on_delete=models.PROTECT
)
attacments = models.ManyToManyField(OffboardingStageMultipleFile)
class OffboardingNote(models.Model):
"""
OffboardingNote
"""
attachments = models.ManyToManyField(
OffboardingStageMultipleFile, blank=True, editable=False
)
title = models.CharField(max_length=20, null=True)
description = models.TextField(null=True, blank=True)
note_by = models.ForeignKey(
Employee, on_delete=models.SET_NULL, null=True, editable=False
)
employee_id = models.ForeignKey(
OffboardingEmployee, on_delete=models.PROTECT, null=True, editable=False
)
stage_id = models.ForeignKey(
OffboardingStage, on_delete=models.PROTECT, null=True, editable=False
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def save(self, *args, **kwargs):
request = getattr(thread_local_middleware._thread_locals, "request", None)
if request:
updated_by = request.user.employee_get
self.note_by = updated_by
if self.employee_id:
self.stage_id = self.employee_id.stage_id
return super().save(*args, **kwargs)

View File

@@ -0,0 +1,3 @@
<form hx-post="{% url 'add-employee' %}?stage_id={{ form.instance.stage_id.id }}&instance_id={{ form.instance.pk }}">
{{ form.as_p }}
</form>

View File

@@ -0,0 +1,6 @@
<form hx-post="{% url "add-offboarding-note" %}?employee_id={{employee.id}}" hx-encoding="multipart/form-data">
{{form.as_p}}
</form>
<script>
$(".col-md-6").removeClass("col-md-6");
</script>

View File

@@ -0,0 +1,43 @@
{% load i18n static %}
<style>
#enlargeImageContainer {
position: absolute;
left: -300px;
top: 100px;
height: 200px;
width: 200px;
}
</style>
<a hx-get="{% url 'add-offboarding-note' %}?employee_id={{ employee.id }}" style="width: 125px;position: sticky;top: 0;" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal" class="mb-3 oh-btn oh-btn--secondary">
<ion-icon name="add-outline" role="img" class="md hydrated" aria-label="add outline"></ion-icon>
{% trans 'Add note' %}
</a>
<ol class="oh-activity-sidebar__qa-list" role="list">
{% for note in employee.offboardingnote_set.all %}
<li class="oh-activity-sidebar__qa-item">
<span class="oh-activity-sidebar__q">{{ note.title }}</span>
<span class="oh-activity-sidebar__a">{{ note.description }}</span>
<div class="d-flex mt-2 mb-2">
{% for attachment in note.attachments.all %}
<a href="{{ attachment.attachment.url }}" rel="noopener noreferrer" target="_blank"><span class="oh-file-icon oh-file-icon--pdf" onmouseover="enlargeImage('{{ attachment.attachment.url }}',$(this))" style="width:40px;height:40px"><img src="{% static 'images/ui/minus-icon.png' %}" style="display:block;width:50%;height:50%" hx-get="{% url 'delete-note-attachment' %}?ids={{ attachment.id }}&employee_id={{ employee.id }}" hx-target="#activitySidebar" onclick="event.stopPropagation();event.preventDefault()" /></span></a>
{% endfor %}
<form hx-post="{% url 'view-offboarding-note' %}?note_id={{ note.id }}&employee_id={{ employee.id }}" hx-target="#noteContainer" class="add-files-form" hx-encoding="multipart/form-data">
{% csrf_token %}
<label for="addFile_20" title="Add Files"><ion-icon name="add-outline" style="font-size: 24px" role="img" class="md hydrated" aria-label="add outline"></ion-icon></label>
<input type="file" name="files" class="d-none" multiple="true" id="addFile_20" onchange="submitForm(this)" />
<input type="submit" class="d-none add_more_submit" value="save" />
</form>
</div>
<span class="oh-activity-sidebar__a">
{% trans 'by' %}
<img src="{{ note.note_by.get_avatar }}" style="width: 1.5em; border-radius: 100%" />
{{ note.note_by.get_full_name }} @{{ note.stage_id }}
</span>
<div style="width: 50%;">
<div id="enlargeImageContainer" class="enlargeImageContainer"></div>
</div>
</li>
{% endfor %}
</ol>

View File

@@ -0,0 +1,3 @@
<form hx-post="{% url "create-offboarding" %}?instance_id={{form.instance.id}}">
{{form.as_p}}
</form>

View File

@@ -0,0 +1,34 @@
{% load i18n %}
<section class="oh-wrapper oh-main__topbar" style="padding-bottom: 1rem;">
<div class="oh-main__titlebar oh-main__titlebar--left oh-d-flex-column--resp oh-mb-3--small">
<h1 class="oh-main__titlebar-title fw-bold">{% trans 'Offboarding' %}</h1>
</div>
<div class="oh-main__titlebar oh-main__titlebar--right oh-d-flex-column--resp oh-mb-3--small">
<div class="oh-input-group oh-input__search-group mr-4">
<ion-icon name="search-outline" class="oh-input-group__icon oh-input-group__icon--left md hydrated" role="img" aria-label="search outline"></ion-icon>
<input name="search" id="pipelineSearch" hx-target="#offboardingContainer" type="text" placeholder="Search" style="margin-right:10px" class="oh-input oh-input__icon mr-3" autocomplete="false" aria-label="Search Input" />
</div>
{% include 'offboarding/pipeline/filter.html' %}
{% if perms.offboarding.add_offboarding %}
<div class="oh-main__titlebar-button-container">
<div class="oh-main__titlebar-button-container">
<a hx-get="{% url 'create-offboarding' %}" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal" class="oh-btn oh-btn--secondary">
<ion-icon name="add-outline"></ion-icon>
{% trans 'Create' %}
</a>
</div>
</div>
{% endif %}
</div>
</section>
<div class="oh-modal" id="offboardingModal" 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="offboardingModalBody"></div>
</div>
</div>

View File

@@ -0,0 +1,73 @@
{% load i18n offboarding_filter %}
<style>
.oh-select-custom {
border: 1px solid hsl(213,22%,84%);
padding: 0.3rem 0.8rem 0.3rem 0.3rem;
border-radius: 0rem;
}
</style>
<div id="messages" class="oh-alert-container"></div>
<div class="oh-wrapper">
<div class="oh-tabs">
<ul class="oh-tabs__tablist">
{% for offboarding in offboardings %}
<li class="oh-tabs__tab" onclick="localStorage.setItem('activeTabOffboarding',$(this).attr('data-target'));" data-target="#Offboarding{{ offboarding.id }}">
{{ offboarding.title }}
<div class="d-flex">
<div class="oh-tabs__input-badge-container">
<span class="oh-badge oh-badge--secondary oh-badge--small oh-badge--round ms-2 mr-2" id="recruitmentCandidateCount1" title="{{ offboarding.offboardingstage_set.all|length }} Stages" onclick="event.stopPropagation()">
{{ offboarding.offboardingstage_set.all|length }}
</span>
</div>
<div class="oh-dropdown" x-data="{open: false}">
<button class="oh-btn oh-stop-prop oh-btn--transparent oh-accordion-meta__btn" @click="open = !open" @click.outside="open = false" title="Actions">
<ion-icon name="ellipsis-vertical" role="img" class="md hydrated" aria-label="ellipsis vertical"></ion-icon>
</button>
<div class="oh-dropdown__menu oh-dropdown__menu--right" x-show="open" style="display: none;">
<ul class="oh-dropdown__items">
{% if perms.offboarding.change_offboarding or request.user.employee_get|is_offboarding_manager %}
<li class="oh-dropdown__item">
<a hx-get="{% url "create-offboarding" %}?instance_id={{offboarding.id}}" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal" class="oh-dropdown__link">{% trans "Edit" %}</a>
</li>
{% endif %}
{% if perms.offboarding.delete_offboarding %}
<li class="oh-dropdown__item">
<form action="{% url "delete-offboarding" %}" onsubmit="return confirm('Are you sure you want to delete this offboarding?');" method="post">
{% csrf_token %}
<button type="submit" class="oh-dropdown__link oh-dropdown__link--danger">
{% trans "Delete" %}
</button>
</form>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
<div class="oh-tabs__contents">
{% for offboarding in offboardings %}
<div class="oh-tabs__content" id="Offboarding{{ offboarding.id }}">
{% if perms.offboarding.add_offboardingstage or request.user.employee_get|any_manager %}
<a hx-get="{% url 'create-offboarding-stage' %}?offboarding_id={{offboarding.id}}" style="width: 100px;" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal" class="mb-3 oh-btn oh-btn--secondary">
<ion-icon name="add-outline"></ion-icon>
{% trans 'Stage' %}
</a>
{% endif %}
{% include "offboarding/stage/stages.html" %}
</div>
{% endfor %}
</div>
</div>
</div>
<script>
let activeTab = localStorage.getItem("activeTabOffboarding")
if (activeTab) {
$(`.oh-tabs__tab[data-target="${activeTab}"]`).addClass("oh-tabs__tab--active")
$(`${activeTab}`).addClass("oh-tabs__content--active")
}
</script>

View File

@@ -0,0 +1,44 @@
{% extends 'index.html' %}
{% block content %}
<style>
.search-highlight {
background-color: rgba(255, 68, 0, 0.076);
}
</style>
{% include 'offboarding/pipeline/nav.html' %}
{% include 'offboarding/pipeline/offboardings.html' %}
<script>
$('#pipelineSearch').keyup(function (e) {
e.preventDefault()
var search = $(this).val().toLowerCase()
$('[data-employee]').each(function () {
var employeeFullName = $(this).attr('data-employee')
if (employeeFullName.toLowerCase().includes(search)) {
$(this).show()
$(this).addClass('search-highlight')
} else {
$(this).hide()
$(this).removeClass('search-highlight')
}
})
if (search == '') {
$('.search-highlight').removeClass('search-highlight')
$('[data-employee]').show()
}
if (search != '') {
$('#filterTagContainerSectionNav').html('')
$('#filterTagContainerSectionNav').append(
'<span class="oh-titlebar__tag filter-field pipelineSearch">Search :' +
search +
`<button class="oh-titlebar__tag-close" onclick="$('#pipelineSearch').val('');$('#pipelineSearch').keyup()">
<ion-icon name="close-outline">
</ion-icon>
</button>
</span>`
)
} else {
$('#filterTagContainerSectionNav').html('')
}
})
</script>
{% endblock %}

View File

@@ -0,0 +1,3 @@
<form hx-post="{% url 'create-offboarding-stage' %}?offboarding_id={{ form.instance.offboarding_id.id }}&instance_id={{ form.instance.pk }}">
{{ form.as_p }}
</form>

View File

@@ -0,0 +1,96 @@
{% load i18n offboarding_filter %}
{% for stage in offboarding.offboardingstage_set.all %}
<div class="oh-accordion-meta" id="accordion{{stage.id}}">
<div class="oh-accordion-meta__item">
<div class="oh-accordion-meta__header oh-accordion-meta__header--show">
<span class="oh-accordion-meta__title" data-offboarding-id="{{offboarding.id}}">
<span class="d-flex" onclick="event.stopPropagation()">
<span class="oh-badge oh-badge--secondary oh-badge--small oh-badge--round ms-2 mr-2"
id="offboardingBadge{{offboarding.id}}_{{forloop.counter}}" title="{{stage.offboardingemployee_set.all|length}} {% trans "Employees" %}" onclick="event.stopPropagation()">
{{ stage.offboardingemployee_set.all|length }}
</span>
{{stage.title}}
{% if stage.is_archived_stage %}
<div class="oh-switch ml-2">
<input type="checkbox" class="show-archived oh-switch__checkbox" title="{% trans "Toggle Archived" %}" onchange="showArchived($(this))">
</div>
{% endif %}
</span>
</span>
<div class="oh-accordion-meta__actions" onclick="event.stopPropagation()">
<div class="oh-dropdown" x-data="{open: false}">
<button class="oh-btn oh-stop-prop oh-accordion-meta__btn" @click="open = !open"
@click.outside="open = false">
{% trans "Actions" %}
<ion-icon class="ms-2 oh-accordion-meta__btn-icon" name="caret-down-outline"></ion-icon>
</button>
<div class="oh-dropdown__menu oh-dropdown__menu--right" x-show="open">
<ul class="oh-dropdown__items">
{% if perms.offboarding.add_offboardingemployee or request.user.employee_get|any_manager %}
<li class="oh-dropdown__item">
<a hx-get="{% url "add-employee" %}?stage_id={{stage.id}}" data-target="#offboardingModal"
data-toggle="oh-modal-toggle" hx-target="#offboardingModalBody" class="oh-dropdown__link">
{% trans "Add Employee" %}</a>
</li>
{% endif %}
{% if perms.offboarding.change_offboardingstage or request.user.employee_get|is_offboarding_manager %}
<li class="oh-dropdown__item">
<a
hx-get="{% url "create-offboarding-stage" %}?offboarding_id={{offboarding.id}}&instance_id={{stage.id}}"
data-target="#offboardingModal"
data-toggle="oh-modal-toggle" hx-target="#offboardingModalBody" class="oh-dropdown__link">{% trans "Edit" %}</a>
</li>
{% endif %}
{% if perms.offboarding.delete_offboarding %}
<li class="oh-dropdown__item">
<a href="{% url "delete-offboarding-stage" %}?ids={{stage.id}}"
onclick="return confirm('Are you sure want to delete this stage?')"
class="oh-dropdown__link oh-dropdown__link--danger">{% trans "Delete" %}</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
<div class="oh-accordion-meta__body">
<div class="oh-sticky-table oh-sticky-table--no-overflow mb-5">
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Employee" %}</div>
<div class="oh-sticky-table__th">{% trans "Notice Period" %}</div>
<div class="oh-sticky-table__th">{% trans "Start Date" %}</div>
<div class="oh-sticky-table__th">{% trans "End Date" %}</div>
<div class="oh-sticky-table__th">{% trans "Stage" %}</div>
<div class="oh-sticky-table__th">{% trans "Options" %}</div>
{% for task in stage.offboardingtask_set.all %}
<div class="oh-sticky-table__th" style="width: 200px;" hx-get="{% url "offboarding-add-task" %}?stage_id={{stage.id}}&instance_id={{task.id}}" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal">
<div class="d-flex justify-content-between">
<span title="Click to edit">
{{task.title}}
</span>
{% if perms.offboarding.delete_offboardingtask %}
<a class="text-danger" href="{% url 'delete-offboarding-task' %}?task_ids={{task.id}}" title="{% trans "Delete" %}" onclick="event.stopPropagation();return confirm('Do you want to delete task?')"><ion-icon name="trash-outline"></ion-icon></a>
{% endif %}
</div>
</div>
{% endfor %}
<div class="oh-sticky-table__th" style="width: 120px;">
{% if perms.offboarding.add_offboardingtask or request.user.employee_get|any_manager %}
<button class="oh-checkpoint-badge text-success" data-toggle="oh-modal-toggle" data-target="#offboardingModal" hx-get="{% url "offboarding-add-task" %}?stage_id={{stage.id}}" hx-target="#offboardingModalBody">
{% trans "Add Task" %}
</button>
{% endif %}
</div>
</div>
</div>
<div class="oh-sticky-table__tbody" id="tableBody{{stage.id}}" data-stage-id="{{stage.id}}" data-archive-stage="{{stage.is_archived_stage|lower}}" data-offboarding-id="{{offboarding.id}}">
{% include "offboarding/task/table_body.html" %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,59 @@
{% load i18n offboarding_filter %}
<script>
function offboardingUpdateStage($element) {
submitButton = $element.closest("form").find("input[type=submit]")
submitButton.click()
}
</script>
<div class="oh-card" id="offboardingBody{{offboarding.id}}">
{% include "offboarding/stage/offboarding_body.html" %}
</div>
<div class="oh-activity-sidebar" id="activitySidebar">
<div class="oh-activity-sidebar__header">
<a
style="cursor: pointer;"
onclick="$('.oh-activity-sidebar--show').removeClass('oh-activity-sidebar--show');">
<ion-icon
name="chevron-back-outline"
class="oh-activity-sidebar__header-icon me-2 oh-activity-sidebar__close"
data-target="#activitySidebar"
></ion-icon>
</a>
<span class="oh-activity-sidebar__title"> {% trans "Notes" %} </span>
</div>
<div class="oh-activity-sidebar__body" id="noteContainer">
</div>
</div>
</div>
<script>
// This lines is used to set default selected stage for exits lines
function enlargeImage(src,$element) {
var enlargeImageContainer = $element.parents().closest("li").find("#enlargeImageContainer")
enlargeImageContainer.empty()
style = 'width:100%; height:90%; box-shadow: 0 10px 10px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); background:white'
var enlargedImage = $('<iframe>').attr({ src: src, style: style })
var name = $('<span>').text(src.split('/').pop().replace(/_/g, ' '))
enlargeImageContainer.append(enlargedImage)
enlargeImageContainer.append(name)
setTimeout(function () {
enlargeImageContainer.show()
const iframe = document.querySelector('iframe').contentWindow
var iframe_document = iframe.document
iframe_image = iframe_document.getElementsByTagName('img')[0]
$(iframe_image).attr('style', 'width:100%; height:100%;')
}, 100)
}
function hideEnlargeImage() {
var enlargeImageContainer = $('.enlargeImageContainer')
enlargeImageContainer.empty()
}
$(document).on('click', function (event) {
if (!$(event.target).closest('#enlargeImageContainer').length) {
hideEnlargeImage()
}
})
</script>

View File

@@ -0,0 +1,7 @@
<form hx-post="{% url "offboarding-add-task" %}?instance_id={{form.instance.pk}}" hx-target="#offboardingModalBody">
{{form.as_p}}
</form>
<script>
$(".col-md-6").removeClass("col-md-6");
</script>

View File

@@ -0,0 +1,98 @@
{% load i18n offboarding_filter %}
{% for employee in stage.offboardingemployee_set.all %}
<div class="oh-sticky-table__tr oh-multiple-table-sort__movable" data-employee="{{employee.employee_id.get_full_name}}" data-employee-id="{{ employee.id }}">
<div class="oh-sticky-table__sd">
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
<img src="{{ employee.employee_id.get_avatar }}" class="oh-profile__image" />
</div>
<span class="oh-profile__name oh-text--dark">{{ employee.employee_id.get_full_name }}</span>
</div>
</div>
<div class="oh-sticky-table__td">
{% trans 'In' %} {{ employee.notice_period }}
{{ employee.get_unit_display }}
</div>
<div class="oh-sticky-table__td">{{ employee.notice_period_starts }}</div>
<div class="oh-sticky-table__td">{{ employee.notice_period_ends }}</div>
<div class="oh-sticky-table__td">
<form hx-get="{% url "offboarding-change-stage" %}?employee_ids={{employee.id}}"
hx-target="#offboardingBody{{offboarding.id}}">
{{ stage_forms|stages:stage }}
<input type="submit" hidden>
</form>
</div>
<div class="oh-sticky-table__td">
<div class="oh-btn-group">
<button type="button" hx-get="{% url 'send-mail-employee' employee.employee_id.id %}"
title="{% trans 'Send Mail' %}" hx-target="#offboardingModalBody" class="oh-btn oh-btn--light"
data-toggle="oh-modal-toggle" data-target="#offboardingModal"
style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"><ion-icon
name="mail-open-outline"></ion-icon></button>
<button type="button" title="{% trans 'Notes' %}" class="oh-btn oh-btn--light oh-activity-sidebar__open"
data-target="#activitySidebar" hx-get="{% url 'view-offboarding-note' %}?employee_id={{ employee.id }}"
hx-target="#noteContainer" style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"><ion-icon
name="newspaper-outline"></ion-icon>
</button>
{% if not employee.employee_id.is_active %}
<a type="button" href="{% url 'employee-archive' employee.employee_id.id %}" title="{% trans 'Un Archive' %}"
class="oh-btn oh-btn--light tex-primary"
style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"><ion-icon name="archive"></ion-icon></a>
{% else %}
<a type="button" hx-target="#offboardingModalBody" data-toggle="oh-modal-toggle" data-target="#offboardingModal" hx-get="{% url "add-employee" %}?instance_id={{employee.id}}&stage_id={{stage.id}}" title="{% trans 'Edit' %}"
class="oh-btn oh-btn--light tex-primary"
style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"><ion-icon name="create-outline"></ion-icon></a>
{% endif %}
{% if perms.offboarding.delete_offboardingemployee %}
<a type="button" style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;" title="{% trans 'Delete' %}" onclick="return confirm('Do you want to delete this offboarding user?')" class="oh-btn oh-btn--light" href="{% url "delete-offboarding-employee" %}"><ion-icon
name="trash-outline"></ion-icon>
</a>
{% endif %}
</div>
</div>
{% for task in stage.offboardingtask_set.all %}
<div class="oh-sticky-table__td">
{% if task|have_task:employee %}
{% for assinged_tasks in employee|get_assigned_task:task %}
<select hx-get="{% url "update-task-status" %}?stage_id={{stage.id}}&employee_ids={{employee.id}}&task_id={{assinged_tasks.task_id.id}}"
hx-target="#offboardingBody{{offboarding.id}}" name="task_status" id="task_status{{assinged_tasks.id}}"
class="oh-select-custom w-100">
{% for assinged_task in assinged_tasks.statuses %}
{% if assinged_tasks.status == assinged_task.0 %}
<option value="{{ assinged_task.0 }}" selected>{{ assinged_task.1 }}</option>
{% else %}
<option value="{{ assinged_task.0 }}">{{ assinged_task.1 }}</option>
{% endif %}
{% endfor %}
</select>
{% endfor %}
{% else %}
<button hx-get="{% url "offboarding-assign-task" %}?employee_ids={{employee.id}}&task_id={{task.id}}"
hx-target="#offboardingBody{{offboarding.id}}" class="oh-checkpoint-badge text-info"
data-toggle="oh-modal-toggle">{% trans 'Assign' %}</button>
{% endif %}
</div>
{% endfor %}
<div class="oh-sticky-table__td"></div>
</div>
{% endfor %}
<script>
$("[data-archive-stage=true]").find(".oh-sticky-table__tr[data-employee-id]").hide();
function showArchived($element) {
let checked = $element.is(":checked")
if (checked) {
$element.closest(".oh-accordion-meta").find(".oh-sticky-table__tr[data-employee-id]").show()
} else {
$element.closest(".oh-accordion-meta").find(".oh-sticky-table__tr[data-employee-id]").hide()
}
}
var selects = $("[name=stage_id][data-initial-stage]")
$.each(selects, function (indexInArray, valueOfElement) {
$(valueOfElement).val($(valueOfElement).attr("data-initial-stage"))
});
function submitForm(elem) {
$(elem).siblings('.add_more_submit').click()
}
</script>

View File

View File

@@ -0,0 +1,71 @@
"""
offboarding_filter.py
This page is used to write custom template filters.
"""
from django.template.defaultfilters import register
from django import template
from offboarding.models import (
EmployeeTask,
Offboarding,
OffboardingStage,
OffboardingTask,
)
register = template.Library()
@register.filter(name="stages")
def stages(stages_dict, stage):
"""
This method will return stage drop accordingly to the offboarding
"""
form = stages_dict[str(stage.offboarding_id.id)]
attrs = form.fields["stage_id"].widget.attrs
attrs["id"] = "stage" + str(stage.id) + str(stage.offboarding_id.id)
attrs["data-initial-stage"] = stage.id
form.fields["stage_id"].widget.attrs.update(attrs)
return form
@register.filter(name="have_task")
def have_task(task, employee):
"""
used to check the task is for the employee
"""
return EmployeeTask.objects.filter(employee_id=employee, task_id=task).exists()
@register.filter(name="get_assigned_task")
def get_assigned_tak(employee, task):
"""
This method is used to filterout the assigned task
"""
# retun like list to access it in varialbe when first iteration of the loop
return [
EmployeeTask.objects.filter(employee_id=employee, task_id=task).first(),
]
@register.filter(name="any_manager")
def any_manager(employee):
"""
This method is used to check the employee is in managers
employee: Employee model instance
"""
return (
Offboarding.objects.filter(managers=employee).exists()
| OffboardingStage.objects.filter(managers=employee).exists()
| OffboardingTask.objects.filter(managers=employee).exists()
)
@register.filter(name="is_offboarding_manager")
def is_offboarding_manager(employee):
"""
This method is used to check the employee is manager of any offboarding
"""
return Offboarding.objects.filter(managers=employee).exists()

3
offboarding/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

33
offboarding/urls.py Normal file
View File

@@ -0,0 +1,33 @@
"""
offboarding/urls.py
This module is used to register url mappings to functions
"""
from django.urls import path
from offboarding import views
urlpatterns = [
path("offboarding-pipeline", views.pipeline, name="offboarding-pipeline"),
path("create-offboarding", views.create_offboarding, name="create-offboarding"),
path("delete-offboarding", views.delete_offboarding, name="delete-offboarding"),
path(
"create-offboarding-stage", views.create_stage, name="create-offboarding-stage"
),
path("add-employee", views.add_employee, name="add-employee"),
path(
"delete-offboarding-stage", views.delete_stage, name="delete-offboarding-stage"
),
path(
"offboarding-change-stage", views.change_stage, name="offboarding-change-stage"
),
path("view-offboarding-note", views.view_notes, name="view-offboarding-note"),
path("add-offboarding-note", views.add_note, name="add-offboarding-note"),
path(
"delete-note-attachment", views.delete_attachment, name="delete-note-attachment"
),
path("offboarding-add-task", views.add_task, name="offboarding-add-task"),
path("update-task-status", views.update_task_status, name="update-task-status"),
path("offboarding-assign-task", views.task_assign, name="offboarding-assign-task"),
path("delete-offboarding-employee",views.delete_employee,name="delete-offboarding-employee"),
path("delete-offboarding-task",views.delete_task,name="delete-offboarding-task"),
]

359
offboarding/views.py Normal file
View File

@@ -0,0 +1,359 @@
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.contrib import messages
from employee.models import Employee
from horilla.decorators import login_required, permission_required
from offboarding.decorators import any_manager_can_enter, offboarding_manager_can_enter, offboarding_or_stage_manager_can_enter
from offboarding.forms import (
NoteForm,
OffboardingEmployeeForm,
OffboardingForm,
OffboardingStageForm,
StageSelectForm,
TaskForm,
)
from offboarding.models import (
EmployeeTask,
Offboarding,
OffboardingEmployee,
OffboardingNote,
OffboardingStage,
OffboardingStageMultipleFile,
OffboardingTask,
)
# Create your views here.
@login_required
@any_manager_can_enter("offboarding.view_offboarding")
def pipeline(request):
"""
Offboarding pipleine view
"""
offboardings = Offboarding.objects.all()
stage_forms = {}
for offboarding in offboardings:
stage_forms[str(offboarding.id)] = StageSelectForm(offboarding=offboarding)
return render(
request,
"offboarding/pipeline/pipeline.html",
{"offboardings": offboardings, "stage_forms": stage_forms},
)
@login_required
@permission_required("offboarding.add_offboarding")
def create_offboarding(request):
"""
Create offboarding view
"""
instance_id = eval(str(request.GET.get("instance_id")))
instance = None
if instance_id and isinstance(instance_id, int):
instance = Offboarding.objects.filter(id=instance_id).first()
form = OffboardingForm(instance=instance)
if request.method == "POST":
form = OffboardingForm(request.POST, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Offboarding saved")
return HttpResponse("<script>window.location.reload()</script>")
return render(
request,
"offboarding/pipeline/form.html",
{
"form": form,
},
)
@login_required
@permission_required("offboarding.delete_offboarding")
def delete_offboarding(request):
"""
This method is used to delete offboardings
"""
ids = request.GET.getlits("id")
Offboarding.objects.filter(id__in=ids).delete()
messages.success(request, "Offboarding deleted")
return redirect(pipeline)
@login_required
@offboarding_manager_can_enter("offboarding.add_offboardingstage")
def create_stage(request):
"""
This method is used to create stages for offboardings
"""
offboarding_id = request.GET["offboarding_id"]
instance_id = eval(str(request.GET.get("instance_id")))
instance = None
if instance_id and isinstance(instance_id, int):
instance = OffboardingStage.objects.get(id=instance_id)
offboarding = Offboarding.objects.get(id=offboarding_id)
form = OffboardingStageForm(instance=instance)
form.instance.offboarding_id = offboarding
if request.method == "POST":
form = OffboardingStageForm(request.POST, instance=instance)
if form.is_valid():
instance = form.save(commit=False)
instance.offboarding_id = offboarding
instance.save()
instance.managers.set(form.data.getlist("managers"))
messages.success(request, "Stage saved")
return HttpResponse("<script>window.location.reload()</script>")
return render(request, "offboarding/stage/form.html", {"form": form})
@login_required
@any_manager_can_enter("offboarding.add_offboardingemployee")
def add_employee(request):
"""
This method is used to add employee to the stage
"""
stage_id = request.GET["stage_id"]
instance_id = eval(str(request.GET.get("instance_id")))
instance = None
if instance_id and isinstance(instance_id, int):
instance = OffboardingEmployee.objects.get(id=instance_id)
stage = OffboardingStage.objects.get(id=stage_id)
form = OffboardingEmployeeForm(initial={"stage_id": stage}, instance=instance)
form.instance.stage_id = stage
if request.method == "POST":
form = OffboardingEmployeeForm(
request.POST,
instance=instance
)
if form.is_valid():
instance = form.save(commit=False)
instance.stage_id = stage
instance.save()
messages.success(request, "Employee added to the stage")
return HttpResponse("<script>window.location.reload()</script>")
return render(request, "offboarding/employee/form.html", {"form": form})
@login_required
@permission_required("offboarding.delete_offboardingemployee")
def delete_employee(request):
"""
This method is used to delete the offboarding employee
"""
employee_ids = request.GET.getlist("employee_ids")
OffboardingEmployee.objects.filter(id__in=employee_ids).delete()
messages.success(request, "Offboarding employee deleted")
return redirect(pipeline)
@login_required
@permission_required("offboarding.delete_offboardingstage")
def delete_stage(request):
"""
This method is used to delete the offboarding stage
"""
ids = request.GET.getlist("ids")
OffboardingStage.objects.filter(id__in=ids).delete()
messages.success(request, "Stage deleted")
return redirect(pipeline)
@login_required
@any_manager_can_enter("offboarding.change_offboarding")
def change_stage(request):
"""
This method is used to update the stages of the employee
"""
employee_ids = request.GET.getlist("employee_ids")
stage_id = request.GET["stage_id"]
employees = OffboardingEmployee.objects.filter(id__in=employee_ids)
stage = OffboardingStage.objects.get(id=stage_id)
# This wont trigger the save method inside the offboarding employee
# employees.update(stage_id=stage)
for employee in employees:
employee.stage_id = stage
employee.save()
if stage.type == "archived":
Employee.objects.filter(
id__in=employees.values_list("employee_id__id", flat=True)
).update(is_active=False)
stage_forms = {}
stage_forms[str(stage.offboarding_id.id)] = StageSelectForm(
offboarding=stage.offboarding_id
)
return render(
request,
"offboarding/stage/offboarding_body.html",
{"offboarding": stage.offboarding_id, "stage_forms": stage_forms},
)
@login_required
@any_manager_can_enter("offboarding.view_offboardingnote")
def view_notes(request):
"""
This method is used to render all the notes of the employee
"""
if request.FILES:
files = request.FILES.getlist("files")
note_id = request.GET["note_id"]
note = OffboardingNote.objects.get(id=note_id)
attachments = []
for file in files:
attachment = OffboardingStageMultipleFile()
attachment.attachment = file
attachment.save()
attachments.append(attachment)
note.attachments.add(*attachments)
offboarding_employee_id = request.GET["employee_id"]
employee = OffboardingEmployee.objects.get(id=offboarding_employee_id)
return render(
request,
"offboarding/note/view_notes.html",
{
"employee": employee,
},
)
@login_required
@any_manager_can_enter("offboarding.add_offboardingnote")
def add_note(request):
"""
This method is used to create note for the offboarding employee
"""
employee_id = request.GET["employee_id"]
employee = OffboardingEmployee.objects.get(id=employee_id)
form = NoteForm()
if request.method == "POST":
form = NoteForm(request.POST, request.FILES)
form.instance.employee_id = employee
if form.is_valid():
form.save()
return HttpResponse(
f"""
<div id="asfdoiioe09092u09un320" hx-get="/offboarding/view-offboarding-note?employee_id={employee.pk}"
hx-target="#noteContainer"
>
</div>
<script>
$("#asfdoiioe09092u09un320").click()
$(".oh-modal--show").removeClass("oh-modal--show")
</script>
"""
)
return render(
request, "offboarding/note/form.html", {"form": form, "employee": employee}
)
@login_required
@permission_required("offboarding.delete_offboardingnote")
def delete_attachment(request):
"""
Used to delete attachment
"""
ids = request.GET.getlist("ids")
OffboardingStageMultipleFile.objects.filter(id__in=ids).delete()
offboarding_employee_id = request.GET["employee_id"]
employee = OffboardingEmployee.objects.get(id=offboarding_employee_id)
return render(
request,
"offboarding/note/view_notes.html",
{
"employee": employee,
},
)
@login_required
@offboarding_or_stage_manager_can_enter("offboarding.add_offboardingtask")
def add_task(request):
"""
This method is used to add offboarding tasks
"""
stage_id = request.GET.get("stage_id")
instance_id = eval(str(request.GET.get("instance_id")))
employees = OffboardingEmployee.objects.filter(stage_id=stage_id)
instance = None
if instance_id:
instance = OffboardingTask.objects.filter(id=instance_id).first()
form = TaskForm(
initial={
"stage_id": stage_id,
"tasks_to": employees,
},
instance=instance,
)
if request.method == "POST":
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Task Added")
return HttpResponse("<script>window.location.reload()</script>")
return render(request, "offboarding/task/form.html", {"form": form})
@login_required
@any_manager_can_enter("offboarding.change_employeetask")
def update_task_status(request):
"""
This method is used to update the assigned tasks status
"""
stage_id = request.GET["stage_id"]
employee_ids = request.GET.getlist("employee_ids")
task_id = request.GET["task_id"]
status = request.GET["task_status"]
EmployeeTask.objects.filter(
employee_id__id__in=employee_ids, task_id__id=task_id
).update(status=status)
stage = OffboardingStage.objects.get(id=stage_id)
stage_forms = {}
stage_forms[str(stage.offboarding_id.id)] = StageSelectForm(
offboarding=stage.offboarding_id
)
return render(
request,
"offboarding/stage/offboarding_body.html",
{"offboarding": stage.offboarding_id, "stage_forms": stage_forms},
)
@login_required
@any_manager_can_enter("offboarding.add_employeetask")
def task_assign(request):
"""
This method is used to assign task to employees
"""
employee_ids = request.GET.getlist("employee_ids")
task_id = request.GET["task_id"]
employees = OffboardingEmployee.objects.filter(id__in=employee_ids)
task = OffboardingTask.objects.get(id=task_id)
for employee in employees:
assinged_task = EmployeeTask()
assinged_task.employee_id = employee
assinged_task.task_id = task
assinged_task.save()
offboarding = employees.first().stage_id.offboarding_id
stage_forms = {}
stage_forms[str(offboarding.id)] = StageSelectForm(offboarding=offboarding)
return render(
request,
"offboarding/stage/offboarding_body.html",
{"offboarding": offboarding, "stage_forms": stage_forms},
)
@login_required
@permission_required("offboarding.delete_offboardingtask")
def delete_task(request):
"""
This method is used to delete the task
"""
task_ids = request.GET.getlist("task_ids")
OffboardingTask.objects.filter(id__in=task_ids).delete()
messages.success(request, "Task deleted")
return redirect(pipeline)