[ADD] ONBOARDING: Probation management

This commit is contained in:
Horilla
2024-01-23 15:29:05 +05:30
parent 3bab4e17a9
commit 5d7451a951
7 changed files with 192 additions and 105 deletions

View File

@@ -222,5 +222,7 @@ class OnboardingStageThread(threading.Thread):
c_task.stage_id = c_task.onboarding_task_id.stage_id
c_task.save()
OnboardingStageThread().start()
try:
OnboardingStageThread().start()
except:
pass

View File

@@ -31,10 +31,22 @@
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Joining Till" %}l</label>
<label class="oh-label">{% trans "Joining Till" %}</label>
{{form.scheduled_till}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Probation From" %}</label>
{{form.probation_end_from}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Probation Till" %}</label>
{{form.probation_end_till}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Portal Sent" %}</label>

View File

@@ -18,6 +18,7 @@
<div class="oh-sticky-table__th">{% trans "Candidate" %}</div>
<div class="oh-sticky-table__th">{% trans "Email" %}</div>
<div class="oh-sticky-table__th">{% trans "Date of joining" %}</div>
<div class="oh-sticky-table__th">{% trans "Probation ends" %}</div>
<div class="oh-sticky-table__th">{% trans "Job position" %}</div>
<div class="oh-sticky-table__th">{% trans "Recruitment" %}</div>
<div class="oh-sticky-table__th">{% trans "Actions" %}</div>
@@ -47,9 +48,13 @@
</div>
<a style="color: inherit; text-decoration: none" class="oh-sticky-table__td">{{candidate.email}}</a>
<a style="color: inherit; text-decoration: none" class="oh-sticky-table__td">
<input type="date" class="oh-input joining-date" value={{candidate.joining_date|date:"Y-m-d"}}
<input type="date" class="oh-input joining-date" style="width: 150px;" value={{candidate.joining_date|date:"Y-m-d"}}
name="joining_date" data-candidate-id="{{candidate.id}}">
</a>
<a style="color: inherit; text-decoration: none" class="oh-sticky-table__td">
<input type="date" class="oh-input" style="width: 150px;" value={{candidate.probation_end|date:"Y-m-d"}}
name="probation_end" onchange="probotionEnd($(this))" data-candidate-id="{{candidate.id}}">
</a>
<a style="color: inherit; text-decoration: none" class="oh-sticky-table__td">{{candidate.job_position_id}}</a>
<a style="color: inherit; text-decoration: none" class="oh-sticky-table__td">{{candidate.recruitment_id}}</a>
<div style="color: inherit; text-decoration: none" class="oh-sticky-table__td">
@@ -119,6 +124,25 @@
</div>
<script>
function probotionEnd($element){
candId = $element.attr("data-candidate-id");
dateVal = $element.val()
$.ajax({
type: "get",
url: "{% url 'update-probotion-end' %}",
data: {
"candidate_id":candId,
"probation_end":dateVal,
},
success: function (response) {
$("#ohMessages").append(`
<div class="oh-alert-container">
<div class="oh-alert oh-alert--animated oh-alert--${response.type}">
${response.message}
</div>`);
}
});
}
$(document).ready(function () {
$(".joining-date").change(function (e) {
let candId = $(this).attr("data-candidate-id");

View File

@@ -61,25 +61,25 @@
onclick="$('[name=joining_set]').val('true'); $('[name=joining_set]').first().change(); $('.filterButton').click()"
style="cursor: pointer">
<span class="oh-dot oh-dot--small me-1" style="background-color:yellow"></span>
Joining Set
{% trans "Joining Set" %}
</span>
<span class="m-3"
onclick="$('[name=joining_set]').val('false'); $('[name=joining_set]').first().change(); $('.filterButton').click()"
style="cursor: pointer">
<span class="oh-dot oh-dot--small me-1" style="background-color:burlywood"></span>
Joining Not-Set
{% trans "Joining Not-Set" %}
</span>
<span class="m-3"
onclick="$('[name=portal_sent]').val('false'); $('[name=portal_sent]').first().change(); $('.filterButton').click()"
style="cursor: pointer">
<span class="oh-dot oh-dot--small me-1" style="background-color:rgba(128, 128, 128, 0.482)"></span>
Portal Not-Sent
{% trans "Portal Not-Sent" %}
</span>
<span class="m-3"
onclick="$('[name=portal_sent]').val('true'); $('[name=portal_sent]').first().change(); $('.filterButton').click()"
style="cursor: pointer">
<span class="oh-dot oh-dot--small me-1" style="background-color:yellowgreen"></span>
Portal Sent
{% trans "Portal Sent" %}
</span>
</div>
<div id="messages" class="oh-alert-container">
@@ -101,8 +101,8 @@
<div class="oh-modal" id="addAttachments" role="dialog" aria-labelledby="addAttachments" aria-hidden="true">
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="addEmployeeModalLabel">Attachments <span
style="color: #80808080;font-size: 14px;">(Optional)</span></span>
<span class="oh-modal__dialog-title" id="addEmployeeModalLabel">{% trans "Attachments" %} <span
style="color: #80808080;font-size: 14px;">({% trans "Optional" %})</span></span>
<button class="oh-modal__close" aria-label="Close">
<ion-icon name="close-outline"></ion-icon>
</button>
@@ -122,7 +122,7 @@
</select>
</div>
<div class="oh-input__group">
<label class="oh-input__label mt-0" for="keyTitle">Template as Attachments</label>
<label class="oh-input__label mt-0" for="keyTitle">{% trans "Template as Attachments" %}</label>
<select name="template_attachment_ids" id="template_attachment_ids" multiple
class="oh-select oh-select-2 w-100">
{% for template in mail_templates %}
@@ -131,7 +131,7 @@
</select>
</div>
<div class="oh-input__group">
<label class="oh-input__label" for="keyDesc">Other Attachments</label>
<label class="oh-input__label" for="keyDesc">{% trans "Other Attachments" %}</label>
<input type="file" multiple name="other_attachments" id="other_attachments">
</div>
</div>
@@ -148,7 +148,7 @@
`)
)"
class="oh-btn oh-btn--secondary oh-btn--shadow" id="trigger-onboarding">
Send Portal Link
{% trans "Send Portal Link" %}
</button>
</div>
</form>

View File

@@ -125,4 +125,5 @@ urlpatterns = [
views.onboarding_send_mail,
name="onboarding-send-mail",
),
path("update-probotion-end",views.update_probation_end,name="update-probotion-end")
]

View File

@@ -208,26 +208,33 @@ def task_creation(request):
GET : return onboarding task creation form template
POST : return onboarding view
"""
stage_id = request.GET.get('stage_id')
stage_id = request.GET.get("stage_id")
stage = OnboardingStage.objects.get(id=stage_id)
form = OnboardingViewTaskForm(initial={'stage_id':stage})
form = OnboardingViewTaskForm(initial={"stage_id": stage})
if request.method == "POST":
form_data = OnboardingViewTaskForm(request.POST,initial={'stage_id':stage})
form_data = OnboardingViewTaskForm(request.POST, initial={"stage_id": stage})
if form_data.is_valid():
candidates= form_data.cleaned_data['candidates']
stage_id = form_data.cleaned_data['stage_id']
managers = form_data.cleaned_data['managers']
title = form_data.cleaned_data['task_title']
onboarding_task =OnboardingTask(task_title=title,stage_id=stage_id)
candidates = form_data.cleaned_data["candidates"]
stage_id = form_data.cleaned_data["stage_id"]
managers = form_data.cleaned_data["managers"]
title = form_data.cleaned_data["task_title"]
onboarding_task = OnboardingTask(task_title=title, stage_id=stage_id)
onboarding_task.save()
onboarding_task.employee_id.set(managers)
onboarding_task.candidates.set(candidates)
if candidates:
for cand in candidates:
task = CandidateTask(candidate_id=cand,stage_id=stage_id,onboarding_task_id=onboarding_task)
task = CandidateTask(
candidate_id=cand,
stage_id=stage_id,
onboarding_task_id=onboarding_task,
)
task.save()
users = [manager.employee_user_id for manager in onboarding_task.employee_id.all()]
users = [
manager.employee_user_id
for manager in onboarding_task.employee_id.all()
]
notify.send(
request.user.employee_get,
recipient=users,
@@ -240,20 +247,27 @@ def task_creation(request):
redirect="/onboarding/onboarding-view",
)
response = render(
request, "onboarding/task_form.html", {"form": form,"stage_id":stage_id}
request,
"onboarding/task_form.html",
{"form": form, "stage_id": stage_id},
)
messages.success(request, _("New task created successfully..."))
return HttpResponse(
response.content.decode("utf-8") + "<script>location.reload();</script>"
)
return render(request, "onboarding/task_form.html", {"form": form,"stage_id":stage_id})
return render(
request, "onboarding/task_form.html", {"form": form, "stage_id": stage_id}
)
@login_required
@hx_request_required
@stage_manager_can_enter("onboarding.change_onboardingtask")
def task_update(request, task_id,):
def task_update(
request,
task_id,
):
"""
function used to update onboarding task.
@@ -291,7 +305,10 @@ def task_update(request, task_id,):
response = render(
request,
"onboarding/task_update.html",
{"form": form, "task_id": task_id,},
{
"form": form,
"task_id": task_id,
},
)
return HttpResponse(
response.content.decode("utf-8") + "<script>location.reload();</script>"
@@ -300,7 +317,10 @@ def task_update(request, task_id,):
return render(
request,
"onboarding/task_update.html",
{"form": form, "task_id": task_id,},
{
"form": form,
"task_id": task_id,
},
)
@@ -456,7 +476,7 @@ def candidates_single_view(request, id, **kwargs):
"recruitment": recruitment,
"choices": choices,
"candidate": candidate,
'single_view':True
"single_view": True,
}
return render(
request,
@@ -698,9 +718,7 @@ def onboarding_view(request):
onboarding_stages = OnboardingStage.objects.all()
choices = CandidateTask.choice
previous_data = request.GET.urlencode()
filter_obj = RecruitmentFilter(
request.GET, queryset=recruitments
)
filter_obj = RecruitmentFilter(request.GET, queryset=recruitments)
paginator = Paginator(filter_obj.qs, 4)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
@@ -746,10 +764,10 @@ def kanban_view(request):
# if not CandidateTask.objects.filter(
# candidate_id=candidate, onboarding_task_id=task
# ).exists():
# pass
# CandidateTask(
# candidate_id=candidate, onboarding_task_id=task
# ).save()
# pass
# CandidateTask(
# candidate_id=candidate, onboarding_task_id=task
# ).save()
recruitments = Recruitment.objects.filter(closed=False)
status = "closed"
@@ -763,14 +781,11 @@ def kanban_view(request):
stage_form = OnboardingViewStageForm()
previous_data = request.GET.urlencode()
filter_obj = RecruitmentFilter(
request.GET, queryset=recruitments
)
filter_obj = RecruitmentFilter(request.GET, queryset=recruitments)
paginator = Paginator(filter_obj.qs, 4)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(
request,
@@ -783,7 +798,7 @@ def kanban_view(request):
"stage_form": stage_form,
"status": status,
"choices": choices,
"pd":previous_data,
"pd": previous_data,
},
)
@@ -1050,8 +1065,10 @@ def candidate_task_update(request, taskId):
else:
canId = request.POST.get("candId")
onboarding_task = OnboardingTask.objects.get(id=taskId)
candidate= Candidate.objects.get(id=canId)
candidate_task = CandidateTask.objects.filter(candidate_id=candidate,onboarding_task_id=onboarding_task).first()
candidate = Candidate.objects.get(id=canId)
candidate_task = CandidateTask.objects.filter(
candidate_id=candidate, onboarding_task_id=onboarding_task
).first()
candidate_task.status = status
candidate_task.save()
users = [
@@ -1074,8 +1091,9 @@ def candidate_task_update(request, taskId):
{"message": _("Candidate onboarding stage updated"), "type": "success"}
)
@login_required
def get_status(request,task_id):
def get_status(request, task_id):
"""
htmx function that return the status of candidate task
@@ -1086,29 +1104,34 @@ def get_status(request,task_id):
Returns:
POST : return candidate task template
"""
cand_id = request.GET.get('cand_id')
cand_stage = request.GET.get('cand_stage')
cand_stage_obj=CandidateStage.objects.get(id=cand_stage)
cand_id = request.GET.get("cand_id")
cand_stage = request.GET.get("cand_stage")
cand_stage_obj = CandidateStage.objects.get(id=cand_stage)
onboarding_task = OnboardingTask.objects.get(id=task_id)
candidate= Candidate.objects.get(id=cand_id)
candidate_task = CandidateTask.objects.filter(candidate_id=candidate,onboarding_task_id=onboarding_task).first()
candidate = Candidate.objects.get(id=cand_id)
candidate_task = CandidateTask.objects.filter(
candidate_id=candidate, onboarding_task_id=onboarding_task
).first()
status = candidate_task.status
return render(request,'onboarding/candidate_task.html',
{
'status':status,
'task':onboarding_task,
'candidate':cand_stage_obj,
'second_load':True,
'choices':CandidateTask.choice
}
)
return render(
request,
"onboarding/candidate_task.html",
{
"status": status,
"task": onboarding_task,
"candidate": cand_stage_obj,
"second_load": True,
"choices": CandidateTask.choice,
},
)
@login_required
@all_manager_can_enter("onboarding.change_candidatetask")
def assign_task(request,task_id):
def assign_task(request, task_id):
"""
htmx function that used to assign a onboarding task to candidate
htmx function that used to assign a onboarding task to candidate
Parameters:
request (HttpRequest): The HTTP request object.
@@ -1117,29 +1140,31 @@ def assign_task(request,task_id):
Returns:
POST : return candidate task template
"""
stage_id = request.GET.get('stage_id')
cand_id = request.GET.get('cand_id')
cand_stage = request.GET.get('cand_stage')
cand_stage_obj=CandidateStage.objects.get(id=cand_stage)
onboarding_task =OnboardingTask.objects.get(id=task_id)
candidate=Candidate.objects.get(id=cand_id)
stage_id = request.GET.get("stage_id")
cand_id = request.GET.get("cand_id")
cand_stage = request.GET.get("cand_stage")
cand_stage_obj = CandidateStage.objects.get(id=cand_stage)
onboarding_task = OnboardingTask.objects.get(id=task_id)
candidate = Candidate.objects.get(id=cand_id)
onboarding_stage = OnboardingStage.objects.get(id=stage_id)
cand_task,created= CandidateTask.objects.get_or_create(
cand_task, created = CandidateTask.objects.get_or_create(
candidate_id=candidate,
stage_id = onboarding_stage,
onboarding_task_id = onboarding_task
stage_id=onboarding_stage,
onboarding_task_id=onboarding_task,
)
cand_task.save()
onboarding_task.candidates.add(candidate)
return render(request,'onboarding/candidate_task.html',
{
'status':cand_task.status,
'task':onboarding_task,
'candidate':cand_stage_obj,
'second_load':True,
'choices':CandidateTask.choice
}
)
return render(
request,
"onboarding/candidate_task.html",
{
"status": cand_task.status,
"task": onboarding_task,
"candidate": cand_stage_obj,
"second_load": True,
"choices": CandidateTask.choice,
},
)
@login_required
@@ -1487,3 +1512,15 @@ def onboarding_send_mail(request, candidate_id):
)
return response
@login_required
@stage_manager_can_enter("recruitment.change_stage")
def update_probation_end(request):
"""
This method is used to update the probotion end date
"""
candidate_id = request.GET.getlist("candidate_id")
probation_end = request.GET["probation_end"]
Candidate.objects.filter(id__in=candidate_id).update(probation_end=probation_end)
return JsonResponse({"message":"Probation end date updated","type":"success"})

View File

@@ -77,7 +77,7 @@ class Recruitment(models.Model):
"To close the recruitment, If closed then not visible on pipeline view."
),
)
is_published = models.BooleanField(default = True)
is_published = models.BooleanField(default=True)
is_active = models.BooleanField(
default=True,
help_text=_(
@@ -225,7 +225,11 @@ class Candidate(models.Model):
"""
choices = [("male", _("Male")), ("female", _("Female")), ("other", _("Other"))]
source_choices = [("application", _("Application Form")), ("software", _("Inside software")), ("other", _("Other"))]
source_choices = [
("application", _("Application Form")),
("software", _("Inside software")),
("other", _("Other")),
]
name = models.CharField(max_length=100, null=True, verbose_name=_("Name"))
profile = models.ImageField(upload_to="recruitment/profile", null=True)
portfolio = models.URLField(max_length=200, blank=True)
@@ -293,7 +297,11 @@ class Candidate(models.Model):
max_length=15, choices=choices, null=True, verbose_name=_("Gender")
)
source = models.CharField(
max_length=20, choices=source_choices, null=True, blank=True, verbose_name=_("Source")
max_length=20,
choices=source_choices,
null=True,
blank=True,
verbose_name=_("Source"),
)
start_onboard = models.BooleanField(default=False, verbose_name=_("Start Onboard"))
hired = models.BooleanField(default=False, verbose_name=_("Hired"))
@@ -319,6 +327,7 @@ class Candidate(models.Model):
],
default="not_sent",
)
probation_end = models.DateField(null=True, editable=False)
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
def __str__(self):
@@ -409,6 +418,7 @@ class StageFiles(models.Model):
def __str__(self):
return self.files.name.split("/")[-1]
class StageNote(models.Model):
"""
StageNote model
@@ -518,27 +528,24 @@ class RecruitmentMailTemplate(models.Model):
title = models.CharField(max_length=25, unique=True)
body = models.TextField()
company_id = models.ForeignKey(
Company, null=True, blank=True, on_delete=models.CASCADE,verbose_name="Company"
Company, null=True, blank=True, on_delete=models.CASCADE, verbose_name="Company"
)
class SkillZone(models.Model):
""""
""" "
Model for talent pool
"""
title = models.CharField(max_length=50, verbose_name="Skill Zone")
description = models.TextField(verbose_name=_("Description"))
created_on = models.DateField(
default=django.utils.timezone.now
)
created_on = models.DateField(default=django.utils.timezone.now)
is_active = models.BooleanField(default=True, verbose_name=_("Is Active"))
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
def get_active(self):
return SkillZoneCandidate.objects.filter(is_active=True,skill_zone_id=self)
return SkillZoneCandidate.objects.filter(is_active=True, skill_zone_id=self)
def __str__(self) -> str:
return self.title
@@ -547,19 +554,20 @@ class SkillZoneCandidate(models.Model):
"""
Model for saving candidate data's for future recruitment
"""
skill_zone_id = models.ForeignKey(
SkillZone,
verbose_name=_("Skill Zone"),
related_name="skillzonecandidate_set",
related_name="skillzonecandidate_set",
on_delete=models.PROTECT,
null=True
null=True,
)
candidate_id = models.ForeignKey(
Candidate,
on_delete= models.PROTECT,
on_delete=models.PROTECT,
null=True,
related_name="skillzonecandidate_set",
verbose_name=_("Candidate")
verbose_name=_("Candidate"),
)
# job_position_id=models.ForeignKey(
# JobPosition,
@@ -569,10 +577,7 @@ class SkillZoneCandidate(models.Model):
# verbose_name=_("Job Position")
# )
reason = models.CharField(
max_length=200,
verbose_name=_("Reason")
)
reason = models.CharField(max_length=200, verbose_name=_("Reason"))
is_active = models.BooleanField(default=True, verbose_name=_("Is Active"))
added_on = models.DateField(
default=django.utils.timezone.now,
@@ -595,12 +600,18 @@ class SkillZoneCandidate(models.Model):
class CandidateRating(models.Model):
employee_id = models.ForeignKey(Employee,on_delete=models.PROTECT, related_name="candidate_rating")
candidate_id = models.ForeignKey(Candidate,on_delete=models.PROTECT, related_name="candidate_rating")
rating = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(5)])
employee_id = models.ForeignKey(
Employee, on_delete=models.PROTECT, related_name="candidate_rating"
)
candidate_id = models.ForeignKey(
Candidate, on_delete=models.PROTECT, related_name="candidate_rating"
)
rating = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(5)]
)
class Meta:
unique_together = ['employee_id', 'candidate_id']
unique_together = ["employee_id", "candidate_id"]
def __str__(self) -> str:
return f"{self.employee_id} - {self.candidate_id} rating {self.rating}"