2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
models.py
|
|
|
|
|
|
|
|
|
|
This module is used to register models for recruitment app
|
|
|
|
|
|
|
|
|
|
"""
|
2024-02-06 12:48:58 +05:30
|
|
|
|
2023-08-14 14:49:11 +05:30
|
|
|
import json
|
2024-05-07 12:23:36 +05:30
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
from datetime import date
|
2024-08-12 10:09:50 +05:30
|
|
|
from uuid import uuid4
|
2024-05-07 12:23:36 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
import django
|
2025-05-08 13:18:49 +05:30
|
|
|
import requests
|
2024-05-07 12:23:36 +05:30
|
|
|
from django import forms
|
2023-11-02 12:32:30 +05:30
|
|
|
from django.conf import settings
|
2023-05-10 15:06:57 +05:30
|
|
|
from django.core.exceptions import ValidationError
|
2024-05-07 12:23:36 +05:30
|
|
|
from django.core.files.storage import default_storage
|
|
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
|
|
|
from django.db import models
|
2024-11-29 12:14:44 +05:30
|
|
|
from django.db.models.signals import m2m_changed, post_save
|
2023-11-28 19:51:36 +05:30
|
|
|
from django.dispatch import receiver
|
2025-05-08 13:18:49 +05:30
|
|
|
from django.http import JsonResponse
|
|
|
|
|
from django.urls import reverse_lazy
|
2024-05-07 12:23:36 +05:30
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
|
|
from base.horilla_company_manager import HorillaCompanyManager
|
2024-06-13 12:07:37 +05:30
|
|
|
from base.models import Company, JobPosition
|
2024-05-07 12:23:36 +05:30
|
|
|
from employee.models import Employee
|
2024-03-27 14:20:20 +05:30
|
|
|
from horilla.models import HorillaModel
|
2023-10-26 12:53:36 +05:30
|
|
|
from horilla_audit.methods import get_diff
|
2024-05-07 12:23:36 +05:30
|
|
|
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
|
2025-05-08 13:18:49 +05:30
|
|
|
from horilla_views.cbv_methods import render_template
|
2023-11-28 19:51:36 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
# Create your models here.
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-08-14 14:49:11 +05:30
|
|
|
def validate_mobile(value):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to validate the mobile number using regular expression
|
|
|
|
|
"""
|
|
|
|
|
pattern = r"^\+[0-9 ]+$|^[0-9 ]+$"
|
|
|
|
|
|
|
|
|
|
if re.match(pattern, value) is None:
|
|
|
|
|
if "+" in value:
|
|
|
|
|
raise forms.ValidationError(
|
|
|
|
|
"Invalid input: Plus symbol (+) should only appear at the beginning \
|
|
|
|
|
or no other characters allowed."
|
|
|
|
|
)
|
2023-09-09 14:03:34 +05:30
|
|
|
raise forms.ValidationError(
|
|
|
|
|
"Invalid input: Only digits and spaces are allowed."
|
|
|
|
|
)
|
2023-08-14 14:49:11 +05:30
|
|
|
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
def validate_pdf(value):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to validate pdf
|
|
|
|
|
"""
|
|
|
|
|
ext = os.path.splitext(value.name)[1] # Get file extension
|
2023-06-06 12:27:41 +05:30
|
|
|
if ext.lower() != ".pdf":
|
2023-08-21 17:25:10 +05:30
|
|
|
raise ValidationError(_("File must be a PDF."))
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def validate_image(value):
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
This method is used to validate the image
|
|
|
|
|
"""
|
|
|
|
|
return value
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-08-12 10:09:50 +05:30
|
|
|
def candidate_photo_upload_path(instance, filename):
|
|
|
|
|
ext = filename.split(".")[-1]
|
|
|
|
|
filename = f"{instance.name.replace(' ', '_')}_{filename}_{uuid4()}.{ext}"
|
|
|
|
|
return os.path.join("recruitment/profile/", filename)
|
|
|
|
|
|
|
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class SurveyTemplate(HorillaModel):
|
|
|
|
|
"""
|
|
|
|
|
SurveyTemplate Model
|
|
|
|
|
"""
|
|
|
|
|
|
2025-04-01 12:38:02 +05:30
|
|
|
title = models.CharField(max_length=50, unique=True)
|
2024-04-15 13:37:52 +05:30
|
|
|
description = models.TextField(null=True, blank=True)
|
|
|
|
|
is_general_template = models.BooleanField(default=False, editable=False)
|
|
|
|
|
company_id = models.ForeignKey(
|
2024-08-31 16:49:22 +05:30
|
|
|
Company,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Company"),
|
2024-04-15 13:37:52 +05:30
|
|
|
)
|
2024-11-15 11:01:05 +05:30
|
|
|
objects = HorillaCompanyManager("company_id")
|
2024-04-15 13:37:52 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return self.title
|
|
|
|
|
|
2025-05-28 14:30:01 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Survey Template")
|
|
|
|
|
verbose_name_plural = _("Survey Templates")
|
|
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
|
2024-07-08 14:54:48 +05:30
|
|
|
class Skill(HorillaModel):
|
|
|
|
|
title = models.CharField(max_length=100)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
title = self.title
|
|
|
|
|
self.title = title.capitalize()
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
2025-05-28 14:30:01 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Skill")
|
|
|
|
|
verbose_name_plural = _("Skills")
|
|
|
|
|
|
2024-07-08 14:54:48 +05:30
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class Recruitment(HorillaModel):
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
Recruitment model
|
|
|
|
|
"""
|
|
|
|
|
|
2025-05-28 14:30:01 +05:30
|
|
|
title = models.CharField(
|
|
|
|
|
max_length=50, null=True, blank=True, verbose_name=_("Title")
|
|
|
|
|
)
|
|
|
|
|
description = models.TextField(null=True, verbose_name=_("Description"))
|
2023-08-14 14:49:11 +05:30
|
|
|
is_event_based = models.BooleanField(
|
2023-08-21 17:25:10 +05:30
|
|
|
default=False,
|
2024-02-13 13:47:04 +05:30
|
|
|
help_text=_("To start recruitment for multiple job positions"),
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
|
|
|
|
closed = models.BooleanField(
|
|
|
|
|
default=False,
|
2023-08-21 17:25:10 +05:30
|
|
|
help_text=_(
|
|
|
|
|
"To close the recruitment, If closed then not visible on pipeline view."
|
|
|
|
|
),
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
2024-02-13 13:47:04 +05:30
|
|
|
is_published = models.BooleanField(
|
|
|
|
|
default=True,
|
|
|
|
|
help_text=_(
|
|
|
|
|
"To publish a recruitment in website, if false then it \
|
|
|
|
|
will not appear on open recruitment page."
|
|
|
|
|
),
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name=_("Is Published"),
|
2024-02-13 13:47:04 +05:30
|
|
|
)
|
2023-08-14 14:49:11 +05:30
|
|
|
is_active = models.BooleanField(
|
|
|
|
|
default=True,
|
2023-08-21 17:25:10 +05:30
|
|
|
help_text=_(
|
|
|
|
|
"To archive and un-archive a recruitment, if active is false then it \
|
|
|
|
|
will not appear on recruitment list view."
|
|
|
|
|
),
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
2023-06-06 12:27:41 +05:30
|
|
|
open_positions = models.ManyToManyField(
|
2025-05-28 14:30:01 +05:30
|
|
|
JobPosition,
|
|
|
|
|
related_name="open_positions",
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Job Position"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
job_position_id = models.ForeignKey(
|
2023-06-06 12:27:41 +05:30
|
|
|
JobPosition,
|
2023-09-20 15:03:01 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-06-06 12:27:41 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
db_constraint=False,
|
|
|
|
|
related_name="recruitment",
|
2023-11-20 10:39:10 +05:30
|
|
|
verbose_name=_("Job Position"),
|
2024-02-14 12:55:50 +05:30
|
|
|
editable=False,
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2025-05-28 14:30:01 +05:30
|
|
|
vacancy = models.IntegerField(default=0, null=True, verbose_name=_("Vacancy"))
|
|
|
|
|
recruitment_managers = models.ManyToManyField(Employee, verbose_name=_("Managers"))
|
|
|
|
|
survey_templates = models.ManyToManyField(
|
|
|
|
|
SurveyTemplate, blank=True, verbose_name=_("Survey Templates")
|
|
|
|
|
)
|
2023-06-06 12:27:41 +05:30
|
|
|
company_id = models.ForeignKey(
|
|
|
|
|
Company,
|
2023-09-20 15:03:01 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-06-06 12:27:41 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
2023-11-20 10:39:10 +05:30
|
|
|
verbose_name=_("Company"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
start_date = models.DateField(default=django.utils.timezone.now)
|
|
|
|
|
end_date = models.DateField(blank=True, null=True)
|
2025-05-28 14:30:01 +05:30
|
|
|
skills = models.ManyToManyField(Skill, blank=True, verbose_name=_("Skills"))
|
2025-05-08 13:18:49 +05:30
|
|
|
linkedin_account_id = models.ForeignKey(
|
|
|
|
|
"recruitment.LinkedInAccount",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("LinkedIn Account"),
|
|
|
|
|
)
|
|
|
|
|
linkedin_post_id = models.CharField(max_length=150, null=True, blank=True)
|
|
|
|
|
publish_in_linkedin = models.BooleanField(
|
|
|
|
|
default=True,
|
|
|
|
|
help_text=_(
|
|
|
|
|
"To publish a recruitment in Linkedin, if active is false then it \
|
|
|
|
|
will not post on LinkedIn."
|
|
|
|
|
),
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name=_("Post on LinkedIn"),
|
2025-05-08 13:18:49 +05:30
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager()
|
2024-01-19 09:37:41 +05:30
|
|
|
default = models.manager.Manager()
|
2024-08-28 12:00:34 +05:30
|
|
|
optional_profile_image = models.BooleanField(
|
2025-05-28 14:30:01 +05:30
|
|
|
default=False,
|
|
|
|
|
help_text=_("Profile image not mandatory for candidate creation"),
|
|
|
|
|
verbose_name=_("Optional Profile Image"),
|
2024-08-28 12:00:34 +05:30
|
|
|
)
|
|
|
|
|
optional_resume = models.BooleanField(
|
2025-05-28 14:30:01 +05:30
|
|
|
default=False,
|
|
|
|
|
help_text=_("Resume not mandatory for candidate creation"),
|
|
|
|
|
verbose_name=_("Optional Resume"),
|
2024-08-28 12:00:34 +05:30
|
|
|
)
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
class Meta:
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
Meta class to add the additional info
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
unique_together = [
|
|
|
|
|
(
|
|
|
|
|
"job_position_id",
|
|
|
|
|
"start_date",
|
|
|
|
|
),
|
|
|
|
|
("job_position_id", "start_date", "company_id"),
|
|
|
|
|
]
|
|
|
|
|
permissions = (("archive_recruitment", "Archive Recruitment"),)
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name = _("Recruitment")
|
|
|
|
|
verbose_name_plural = _("Recruitments")
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2024-02-13 13:47:04 +05:30
|
|
|
def total_hires(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to get the count of
|
|
|
|
|
hired candidates
|
|
|
|
|
"""
|
|
|
|
|
return self.candidate.filter(hired=True).count()
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
def __str__(self):
|
2023-06-06 12:27:41 +05:30
|
|
|
title = (
|
|
|
|
|
f"{self.job_position_id.job_position} {self.start_date}"
|
|
|
|
|
if self.title is None and self.job_position_id
|
|
|
|
|
else self.title
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not self.is_event_based and self.job_position_id is not None:
|
|
|
|
|
self.open_positions.add(self.job_position_id)
|
|
|
|
|
|
|
|
|
|
return title
|
|
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
if self.title is None:
|
2023-08-21 17:25:10 +05:30
|
|
|
raise ValidationError({"title": _("This field is required")})
|
2024-07-08 14:54:48 +05:30
|
|
|
if self.is_published:
|
|
|
|
|
if self.vacancy <= 0:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
_(
|
|
|
|
|
"Vacancy must be greater than zero if the recruitment is publishing."
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-08-21 17:25:10 +05:30
|
|
|
if self.end_date is not None and (
|
2023-09-20 15:03:01 +05:30
|
|
|
self.start_date is not None and self.start_date > self.end_date
|
2023-08-21 17:25:10 +05:30
|
|
|
):
|
2023-08-14 14:49:11 +05:30
|
|
|
raise ValidationError(
|
2023-08-21 17:25:10 +05:30
|
|
|
{"end_date": _("End date cannot be less than start date.")}
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
2023-06-06 12:27:41 +05:30
|
|
|
return super().clean()
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
2025-05-08 13:18:49 +05:30
|
|
|
if not self.publish_in_linkedin:
|
|
|
|
|
self.linkedin_account_id = None
|
|
|
|
|
self.linkedin_post_id = None
|
2023-06-06 12:27:41 +05:30
|
|
|
super().save(*args, **kwargs) # Save the Recruitment instance first
|
|
|
|
|
if self.is_event_based and self.open_positions is None:
|
2023-08-21 17:25:10 +05:30
|
|
|
raise ValidationError({"open_positions": _("This field is required")})
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2024-03-28 10:04:56 +05:30
|
|
|
def ordered_stages(self):
|
|
|
|
|
"""
|
2024-04-15 13:37:52 +05:30
|
|
|
This method will returns all the stage respectively to the ascending order of stages
|
2024-03-28 10:04:56 +05:30
|
|
|
"""
|
|
|
|
|
return self.stage_set.order_by("sequence")
|
2024-05-07 12:23:36 +05:30
|
|
|
|
2024-06-25 15:49:56 +05:30
|
|
|
def is_vacancy_filled(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to check wether the vaccancy for the recruitment is completed or not
|
|
|
|
|
"""
|
|
|
|
|
hired_stage = Stage.objects.filter(
|
|
|
|
|
recruitment_id=self, stage_type="hired"
|
|
|
|
|
).first()
|
|
|
|
|
if hired_stage:
|
|
|
|
|
hired_candidate = hired_stage.candidate_set.all().exclude(canceled=True)
|
|
|
|
|
if len(hired_candidate) >= self.vacancy:
|
|
|
|
|
return True
|
|
|
|
|
|
2024-05-07 12:23:36 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class Stage(HorillaModel):
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
Stage model
|
|
|
|
|
"""
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
stage_types = [
|
2023-06-06 12:27:41 +05:30
|
|
|
("initial", _("Initial")),
|
2024-10-17 16:35:02 +05:30
|
|
|
("applied", _("Applied")),
|
2023-06-06 12:27:41 +05:30
|
|
|
("test", _("Test")),
|
|
|
|
|
("interview", _("Interview")),
|
2024-02-13 13:47:04 +05:30
|
|
|
("cancelled", _("Cancelled")),
|
2023-06-06 12:27:41 +05:30
|
|
|
("hired", _("Hired")),
|
2023-05-10 15:06:57 +05:30
|
|
|
]
|
2023-06-06 12:27:41 +05:30
|
|
|
recruitment_id = models.ForeignKey(
|
|
|
|
|
Recruitment,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="stage_set",
|
2023-11-20 10:39:10 +05:30
|
|
|
verbose_name=_("Recruitment"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2025-05-28 14:30:01 +05:30
|
|
|
stage_managers = models.ManyToManyField(Employee, verbose_name=_("Stage Managers"))
|
|
|
|
|
stage = models.CharField(max_length=50, verbose_name=_("Stage"))
|
2023-06-06 12:27:41 +05:30
|
|
|
stage_type = models.CharField(
|
2025-05-28 14:30:01 +05:30
|
|
|
max_length=20,
|
|
|
|
|
choices=stage_types,
|
|
|
|
|
default="interview",
|
|
|
|
|
verbose_name=_("Stage Type"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2023-08-14 14:49:11 +05:30
|
|
|
sequence = models.IntegerField(null=True, default=0)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
def __str__(self):
|
2023-06-06 12:27:41 +05:30
|
|
|
return f"{self.stage}"
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
class Meta:
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
Meta class to add the additional info
|
|
|
|
|
"""
|
2023-10-05 14:43:10 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
permissions = (("archive_Stage", "Archive Stage"),)
|
|
|
|
|
unique_together = ["recruitment_id", "stage"]
|
2023-07-27 15:27:50 +05:30
|
|
|
ordering = ["sequence"]
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name = _("Stage")
|
|
|
|
|
verbose_name_plural = _("Stages")
|
2023-07-27 15:27:50 +05:30
|
|
|
|
2023-10-05 14:43:10 +05:30
|
|
|
def active_candidates(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to get all the active candidate like related objects
|
|
|
|
|
"""
|
|
|
|
|
return {
|
|
|
|
|
"all": Candidate.objects.filter(
|
|
|
|
|
stage_id=self, canceled=False, is_active=True
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class Candidate(HorillaModel):
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
Candidate model
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
choices = [("male", _("Male")), ("female", _("Female")), ("other", _("Other"))]
|
2024-02-06 12:48:58 +05:30
|
|
|
offer_letter_statuses = [
|
2025-05-28 14:30:01 +05:30
|
|
|
("not_sent", _("Not Sent")),
|
|
|
|
|
("sent", _("Sent")),
|
|
|
|
|
("accepted", _("Accepted")),
|
|
|
|
|
("rejected", _("Rejected")),
|
|
|
|
|
("joined", _("Joined")),
|
2024-02-06 12:48:58 +05:30
|
|
|
]
|
2024-01-23 15:29:05 +05:30
|
|
|
source_choices = [
|
|
|
|
|
("application", _("Application Form")),
|
|
|
|
|
("software", _("Inside software")),
|
|
|
|
|
("other", _("Other")),
|
|
|
|
|
]
|
2023-11-03 13:53:34 +05:30
|
|
|
name = models.CharField(max_length=100, null=True, verbose_name=_("Name"))
|
2024-08-12 10:09:50 +05:30
|
|
|
profile = models.ImageField(upload_to=candidate_photo_upload_path, null=True)
|
2023-06-06 12:27:41 +05:30
|
|
|
portfolio = models.URLField(max_length=200, blank=True)
|
|
|
|
|
recruitment_id = models.ForeignKey(
|
|
|
|
|
Recruitment,
|
2023-09-20 15:03:01 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-06-06 12:27:41 +05:30
|
|
|
null=True,
|
|
|
|
|
related_name="candidate",
|
2023-11-20 10:39:10 +05:30
|
|
|
verbose_name=_("Recruitment"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
|
|
|
|
job_position_id = models.ForeignKey(
|
2023-11-03 13:53:34 +05:30
|
|
|
JobPosition,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Job Position"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
|
|
|
|
stage_id = models.ForeignKey(
|
2023-11-20 10:39:10 +05:30
|
|
|
Stage,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
null=True,
|
|
|
|
|
verbose_name=_("Stage"),
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2024-06-13 12:07:37 +05:30
|
|
|
converted_employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
related_name="candidate_get",
|
|
|
|
|
verbose_name=_("Employee"),
|
|
|
|
|
)
|
2023-11-03 13:53:34 +05:30
|
|
|
schedule_date = models.DateTimeField(
|
|
|
|
|
blank=True, null=True, verbose_name=_("Schedule date")
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2023-11-03 13:53:34 +05:30
|
|
|
email = models.EmailField(max_length=254, unique=True, verbose_name=_("Email"))
|
2023-08-14 14:49:11 +05:30
|
|
|
mobile = models.CharField(
|
|
|
|
|
max_length=15,
|
|
|
|
|
blank=True,
|
|
|
|
|
validators=[
|
|
|
|
|
validate_mobile,
|
|
|
|
|
],
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name=_("Mobile"),
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
2023-06-06 12:27:41 +05:30
|
|
|
resume = models.FileField(
|
|
|
|
|
upload_to="recruitment/resume",
|
|
|
|
|
validators=[
|
|
|
|
|
validate_pdf,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
referral = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
related_name="candidate_referral",
|
2023-11-03 13:53:34 +05:30
|
|
|
verbose_name=_("Referral"),
|
|
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
address = models.TextField(
|
|
|
|
|
null=True, blank=True, verbose_name=_("Address"), max_length=255
|
|
|
|
|
)
|
2023-11-03 13:53:34 +05:30
|
|
|
country = models.CharField(
|
|
|
|
|
max_length=30, null=True, blank=True, verbose_name=_("Country")
|
|
|
|
|
)
|
|
|
|
|
dob = models.DateField(null=True, blank=True, verbose_name=_("Date of Birth"))
|
|
|
|
|
state = models.CharField(
|
|
|
|
|
max_length=30, null=True, blank=True, verbose_name=_("State")
|
|
|
|
|
)
|
|
|
|
|
city = models.CharField(
|
|
|
|
|
max_length=30, null=True, blank=True, verbose_name=_("City")
|
|
|
|
|
)
|
|
|
|
|
zip = models.CharField(
|
|
|
|
|
max_length=30, null=True, blank=True, verbose_name=_("Zip Code")
|
|
|
|
|
)
|
|
|
|
|
gender = models.CharField(
|
2024-03-10 19:37:46 +05:30
|
|
|
max_length=15,
|
|
|
|
|
choices=choices,
|
|
|
|
|
null=True,
|
|
|
|
|
default="male",
|
|
|
|
|
verbose_name=_("Gender"),
|
2023-11-03 13:53:34 +05:30
|
|
|
)
|
2024-01-05 14:35:11 +05:30
|
|
|
source = models.CharField(
|
2024-01-23 15:29:05 +05:30
|
|
|
max_length=20,
|
|
|
|
|
choices=source_choices,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Source"),
|
2024-01-05 14:35:11 +05:30
|
|
|
)
|
2023-11-03 13:53:34 +05:30
|
|
|
start_onboard = models.BooleanField(default=False, verbose_name=_("Start Onboard"))
|
|
|
|
|
hired = models.BooleanField(default=False, verbose_name=_("Hired"))
|
|
|
|
|
canceled = models.BooleanField(default=False, verbose_name=_("Canceled"))
|
2024-12-07 15:13:37 +05:30
|
|
|
converted = models.BooleanField(default=False, verbose_name=_("Converted"))
|
2023-11-03 13:53:34 +05:30
|
|
|
joining_date = models.DateField(
|
|
|
|
|
blank=True, null=True, verbose_name=_("Joining Date")
|
2023-06-06 12:27:41 +05:30
|
|
|
)
|
2023-10-26 12:53:36 +05:30
|
|
|
history = HorillaAuditLog(
|
|
|
|
|
related_name="history_set",
|
|
|
|
|
bases=[
|
|
|
|
|
HorillaAuditInfo,
|
|
|
|
|
],
|
2023-05-10 15:06:57 +05:30
|
|
|
)
|
2023-08-14 14:49:11 +05:30
|
|
|
sequence = models.IntegerField(null=True, default=0)
|
2024-02-09 11:52:34 +05:30
|
|
|
|
2024-01-23 15:29:05 +05:30
|
|
|
probation_end = models.DateField(null=True, editable=False)
|
2024-02-06 12:48:58 +05:30
|
|
|
offer_letter_status = models.CharField(
|
|
|
|
|
max_length=10,
|
|
|
|
|
choices=offer_letter_statuses,
|
|
|
|
|
default="not_sent",
|
|
|
|
|
editable=False,
|
|
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
|
2024-02-13 13:47:04 +05:30
|
|
|
last_updated = models.DateField(null=True, auto_now=True)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-11-05 15:53:54 +05:30
|
|
|
converted_employee_id.exclude_from_automation = True
|
|
|
|
|
mail_to_related_fields = [
|
|
|
|
|
("stage_id__stage_managers__get_mail", "Stage Managers"),
|
|
|
|
|
("recruitment_id__recruitment_managers__get_mail", "Recruitment Managers"),
|
|
|
|
|
]
|
2025-04-01 12:38:02 +05:30
|
|
|
hired_date = models.DateField(null=True, blank=True, editable=False)
|
2024-11-05 15:53:54 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
def __str__(self):
|
2023-06-06 12:27:41 +05:30
|
|
|
return f"{self.name}"
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-02-06 12:48:58 +05:30
|
|
|
def is_offer_rejected(self):
|
|
|
|
|
"""
|
|
|
|
|
Is offer rejected checking method
|
|
|
|
|
"""
|
|
|
|
|
first = RejectedCandidate.objects.filter(candidate_id=self).first()
|
|
|
|
|
if first:
|
|
|
|
|
return first.reject_reason_id.count() > 0
|
|
|
|
|
return first
|
2024-02-13 13:47:04 +05:30
|
|
|
|
2023-10-26 12:53:36 +05:30
|
|
|
def get_full_name(self):
|
|
|
|
|
"""
|
|
|
|
|
Method will return employee full name
|
|
|
|
|
"""
|
|
|
|
|
return str(self.name)
|
|
|
|
|
|
|
|
|
|
def get_avatar(self):
|
|
|
|
|
"""
|
2024-03-27 14:20:20 +05:30
|
|
|
Method will rerun the api to the avatar or path to the profile image
|
2023-10-26 12:53:36 +05:30
|
|
|
"""
|
|
|
|
|
url = (
|
|
|
|
|
f"https://ui-avatars.com/api/?name={self.get_full_name()}&background=random"
|
|
|
|
|
)
|
|
|
|
|
if self.profile:
|
2024-10-10 11:40:53 +05:30
|
|
|
full_filename = self.profile.name
|
2023-11-17 16:48:51 +05:30
|
|
|
|
2023-11-03 13:53:34 +05:30
|
|
|
if default_storage.exists(full_filename):
|
2023-11-02 12:32:30 +05:30
|
|
|
url = self.profile.url
|
|
|
|
|
|
2023-10-26 12:53:36 +05:30
|
|
|
return url
|
|
|
|
|
|
2023-12-21 17:15:25 +05:30
|
|
|
def get_company(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to return the company
|
|
|
|
|
"""
|
|
|
|
|
return getattr(
|
|
|
|
|
getattr(getattr(self, "recruitment_id", None), "company_id", None),
|
|
|
|
|
"company",
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_job_position(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to return the job position of the candidate
|
|
|
|
|
"""
|
|
|
|
|
return self.job_position_id.job_position
|
|
|
|
|
|
|
|
|
|
def get_email(self):
|
|
|
|
|
"""
|
|
|
|
|
Return email
|
|
|
|
|
"""
|
|
|
|
|
return self.email
|
|
|
|
|
|
2024-06-12 16:44:05 +05:30
|
|
|
def get_mail(self):
|
|
|
|
|
""" """
|
|
|
|
|
return self.get_email()
|
|
|
|
|
|
2025-02-28 11:11:57 +05:30
|
|
|
def phone(self):
|
|
|
|
|
return self.mobile
|
|
|
|
|
|
2023-10-26 12:53:36 +05:30
|
|
|
def tracking(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to return the tracked history of the instance
|
|
|
|
|
"""
|
|
|
|
|
return get_diff(self)
|
|
|
|
|
|
2024-01-29 11:48:32 +05:30
|
|
|
def get_last_sent_mail(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to get last send mail
|
|
|
|
|
"""
|
2024-06-13 12:07:37 +05:30
|
|
|
from base.models import EmailLog
|
|
|
|
|
|
2024-01-29 11:48:32 +05:30
|
|
|
return (
|
|
|
|
|
EmailLog.objects.filter(to__icontains=self.email)
|
|
|
|
|
.order_by("-created_at")
|
|
|
|
|
.first()
|
|
|
|
|
)
|
|
|
|
|
|
2024-05-07 20:00:07 +05:30
|
|
|
def get_interview(self):
|
|
|
|
|
"""
|
|
|
|
|
This method is used to get the interview dates and times for the candidate for the mail templates
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
interviews = InterviewSchedule.objects.filter(candidate_id=self.id)
|
|
|
|
|
if interviews:
|
|
|
|
|
interview_info = "<table>"
|
|
|
|
|
interview_info += "<tr><th>Sl No.</th><th>Date</th><th>Time</th><th>Is Completed</th></tr>"
|
|
|
|
|
for index, interview in enumerate(interviews, start=1):
|
|
|
|
|
interview_info += f"<tr><td>{index}</td>"
|
2024-05-08 16:17:10 +05:30
|
|
|
interview_info += (
|
|
|
|
|
f"<td class='dateformat_changer'>{interview.interview_date}</td>"
|
|
|
|
|
)
|
|
|
|
|
interview_info += (
|
|
|
|
|
f"<td class='timeformat_changer'>{interview.interview_time}</td>"
|
|
|
|
|
)
|
|
|
|
|
interview_info += (
|
|
|
|
|
f"<td>{'Yes' if interview.completed else 'No'}</td></tr>"
|
|
|
|
|
)
|
2024-05-07 20:00:07 +05:30
|
|
|
interview_info += "</table>"
|
|
|
|
|
return interview_info
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
def save(self, *args, **kwargs):
|
2023-06-06 12:27:41 +05:30
|
|
|
if self.stage_id is not None:
|
2025-05-14 14:39:08 +05:30
|
|
|
self.hired = self.stage_id.stage_type == "hired"
|
2024-02-16 16:57:43 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
if not self.recruitment_id.is_event_based and self.job_position_id is None:
|
|
|
|
|
self.job_position_id = self.recruitment_id.job_position_id
|
|
|
|
|
if self.job_position_id not in self.recruitment_id.open_positions.all():
|
2023-08-21 17:25:10 +05:30
|
|
|
raise ValidationError({"job_position_id": _("Choose valid choice")})
|
2023-06-06 12:27:41 +05:30
|
|
|
if self.recruitment_id.is_event_based and self.job_position_id is None:
|
2023-08-21 17:25:10 +05:30
|
|
|
raise ValidationError({"job_position_id": _("This field is required.")})
|
2024-04-15 13:37:52 +05:30
|
|
|
if self.stage_id and self.stage_id.stage_type == "cancelled":
|
2024-02-13 13:47:04 +05:30
|
|
|
self.canceled = True
|
|
|
|
|
if self.canceled:
|
|
|
|
|
cancelled_stage = Stage.objects.filter(
|
|
|
|
|
recruitment_id=self.recruitment_id, stage_type="cancelled"
|
|
|
|
|
).first()
|
|
|
|
|
if not cancelled_stage:
|
|
|
|
|
cancelled_stage = Stage.objects.create(
|
|
|
|
|
recruitment_id=self.recruitment_id,
|
|
|
|
|
stage="Cancelled Candidates",
|
|
|
|
|
stage_type="cancelled",
|
|
|
|
|
sequence=50,
|
|
|
|
|
)
|
|
|
|
|
self.stage_id = cancelled_stage
|
2024-06-13 12:07:37 +05:30
|
|
|
if (
|
|
|
|
|
self.converted_employee_id
|
|
|
|
|
and Candidate.objects.filter(
|
|
|
|
|
converted_employee_id=self.converted_employee_id
|
|
|
|
|
)
|
|
|
|
|
.exclude(id=self.id)
|
|
|
|
|
.exists()
|
|
|
|
|
):
|
|
|
|
|
raise ValidationError(_("Employee is uniques for candidate"))
|
|
|
|
|
|
2024-12-07 15:13:37 +05:30
|
|
|
if self.converted:
|
|
|
|
|
self.hired = False
|
|
|
|
|
self.canceled = False
|
|
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
super().save(*args, **kwargs)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
class Meta:
|
|
|
|
|
"""
|
|
|
|
|
Meta class to add the additional info
|
|
|
|
|
"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
unique_together = (
|
|
|
|
|
"email",
|
|
|
|
|
"recruitment_id",
|
|
|
|
|
)
|
|
|
|
|
permissions = (
|
|
|
|
|
("view_history", "View Candidate History"),
|
|
|
|
|
("archive_candidate", "Archive Candidate"),
|
|
|
|
|
)
|
2023-07-26 14:17:40 +05:30
|
|
|
ordering = ["sequence"]
|
2025-05-28 14:30:01 +05:30
|
|
|
verbose_name = _("Candidate")
|
|
|
|
|
verbose_name_plural = _("Candidates")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class RejectReason(HorillaModel):
|
2024-02-06 12:48:58 +05:30
|
|
|
"""
|
|
|
|
|
RejectReason
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
title = models.CharField(
|
2025-04-01 12:38:02 +05:30
|
|
|
max_length=50,
|
2024-02-06 12:48:58 +05:30
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
description = models.TextField(null=True, blank=True, max_length=255)
|
2024-02-06 12:48:58 +05:30
|
|
|
company_id = models.ForeignKey(
|
2024-07-05 16:21:05 +05:30
|
|
|
Company,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Company"),
|
2024-02-06 12:48:58 +05:30
|
|
|
)
|
|
|
|
|
objects = HorillaCompanyManager()
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return self.title
|
|
|
|
|
|
2025-05-28 14:30:01 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Reject Reason")
|
|
|
|
|
verbose_name_plural = _("Reject Reasons")
|
|
|
|
|
|
2024-02-06 12:48:58 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class RejectedCandidate(HorillaModel):
|
2024-02-06 12:48:58 +05:30
|
|
|
"""
|
|
|
|
|
RejectedCandidate
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
candidate_id = models.OneToOneField(
|
2024-02-13 13:47:04 +05:30
|
|
|
Candidate,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
verbose_name="Candidate",
|
|
|
|
|
related_name="rejected_candidate",
|
2024-02-06 12:48:58 +05:30
|
|
|
)
|
|
|
|
|
reject_reason_id = models.ManyToManyField(
|
2024-02-13 13:47:04 +05:30
|
|
|
RejectReason, verbose_name="Reject reason", blank=True
|
2024-02-06 12:48:58 +05:30
|
|
|
)
|
2024-02-14 12:55:50 +05:30
|
|
|
description = models.TextField(max_length=255)
|
2024-03-27 14:20:20 +05:30
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="candidate_id__recruitment_id__company_id"
|
|
|
|
|
)
|
2024-02-07 17:27:37 +05:30
|
|
|
history = HorillaAuditLog(
|
|
|
|
|
related_name="history_set",
|
|
|
|
|
bases=[
|
|
|
|
|
HorillaAuditInfo,
|
|
|
|
|
],
|
|
|
|
|
)
|
2024-02-06 12:48:58 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return super().__str__()
|
|
|
|
|
|
|
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class StageFiles(HorillaModel):
|
2024-01-17 11:24:47 +05:30
|
|
|
files = models.FileField(upload_to="recruitment/stageFiles", blank=True, null=True)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.files.name.split("/")[-1]
|
|
|
|
|
|
2024-01-23 15:29:05 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class StageNote(HorillaModel):
|
2023-06-06 12:27:41 +05:30
|
|
|
"""
|
|
|
|
|
StageNote model
|
|
|
|
|
"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
candidate_id = models.ForeignKey(Candidate, on_delete=models.CASCADE)
|
2024-03-10 19:37:46 +05:30
|
|
|
description = models.TextField(verbose_name=_("Description"), max_length=255)
|
2023-06-06 12:27:41 +05:30
|
|
|
stage_id = models.ForeignKey(Stage, on_delete=models.CASCADE)
|
2024-01-17 11:24:47 +05:30
|
|
|
stage_files = models.ManyToManyField(StageFiles, blank=True)
|
2024-11-29 12:14:44 +05:30
|
|
|
updated_by = models.ForeignKey(
|
|
|
|
|
Employee, on_delete=models.CASCADE, null=True, blank=True
|
|
|
|
|
)
|
|
|
|
|
candidate_can_view = models.BooleanField(default=False)
|
2023-12-21 17:15:25 +05:30
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="candidate_id__recruitment_id__company_id"
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-06-06 12:27:41 +05:30
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.description}"
|
2023-08-14 14:49:11 +05:30
|
|
|
|
2024-11-29 12:14:44 +05:30
|
|
|
def updated_user(self):
|
|
|
|
|
if self.updated_by:
|
|
|
|
|
return self.updated_by
|
|
|
|
|
else:
|
|
|
|
|
return self.candidate_id
|
|
|
|
|
|
2023-08-14 14:49:11 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class RecruitmentSurvey(HorillaModel):
|
2023-08-14 14:49:11 +05:30
|
|
|
"""
|
|
|
|
|
RecruitmentSurvey model
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
question_types = [
|
2023-08-22 13:57:44 +05:30
|
|
|
("checkbox", _("Yes/No")),
|
|
|
|
|
("options", _("Choices")),
|
|
|
|
|
("multiple", _("Multiple Choice")),
|
|
|
|
|
("text", _("Text")),
|
|
|
|
|
("number", _("Number")),
|
|
|
|
|
("percentage", _("Percentage")),
|
|
|
|
|
("date", _("Date")),
|
|
|
|
|
("textarea", _("Textarea")),
|
|
|
|
|
("file", _("File Upload")),
|
|
|
|
|
("rating", _("Rating")),
|
2023-08-14 14:49:11 +05:30
|
|
|
]
|
2024-03-10 19:37:46 +05:30
|
|
|
question = models.TextField(null=False, max_length=255)
|
2024-02-15 12:21:39 +05:30
|
|
|
template_id = models.ManyToManyField(
|
|
|
|
|
SurveyTemplate, verbose_name="Template", blank=True
|
|
|
|
|
)
|
2024-07-05 16:21:05 +05:30
|
|
|
is_mandatory = models.BooleanField(default=False)
|
2023-11-20 10:39:10 +05:30
|
|
|
recruitment_ids = models.ManyToManyField(
|
|
|
|
|
Recruitment,
|
|
|
|
|
verbose_name=_("Recruitment"),
|
|
|
|
|
)
|
2024-07-05 16:21:05 +05:30
|
|
|
question = models.TextField(null=False)
|
2023-11-20 10:39:10 +05:30
|
|
|
job_position_ids = models.ManyToManyField(
|
2024-02-15 12:21:39 +05:30
|
|
|
JobPosition, verbose_name=_("Job Positions"), editable=False
|
2023-11-20 10:39:10 +05:30
|
|
|
)
|
2023-08-14 14:49:11 +05:30
|
|
|
sequence = models.IntegerField(null=True, default=0)
|
|
|
|
|
type = models.CharField(
|
|
|
|
|
max_length=15,
|
|
|
|
|
choices=question_types,
|
|
|
|
|
)
|
|
|
|
|
options = models.TextField(
|
2024-03-10 19:37:46 +05:30
|
|
|
null=True, default="", help_text=_("Separate choices by ', '"), max_length=255
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager(related_company_field="recruitment_ids__company_id")
|
2023-08-14 14:49:11 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return str(self.question)
|
|
|
|
|
|
|
|
|
|
def choices(self):
|
|
|
|
|
"""
|
|
|
|
|
Used to split the choices
|
|
|
|
|
"""
|
|
|
|
|
return self.options.split(", ")
|
|
|
|
|
|
2024-02-15 12:21:39 +05:30
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
if self.template_id is None:
|
|
|
|
|
general_template = SurveyTemplate.objects.filter(
|
|
|
|
|
is_general_template=True
|
|
|
|
|
).first()
|
|
|
|
|
if general_template:
|
|
|
|
|
self.template_id.add(general_template)
|
|
|
|
|
super().save(*args, **kwargs)
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-02-15 12:21:39 +05:30
|
|
|
class Meta:
|
2024-03-10 19:37:46 +05:30
|
|
|
ordering = [
|
|
|
|
|
"sequence",
|
|
|
|
|
]
|
2024-02-15 12:21:39 +05:30
|
|
|
|
|
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class QuestionOrdering(HorillaModel):
|
2024-02-15 12:21:39 +05:30
|
|
|
"""
|
2024-04-15 13:37:52 +05:30
|
|
|
Survey Template model
|
2024-02-15 12:21:39 +05:30
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
question_id = models.ForeignKey(RecruitmentSurvey, on_delete=models.CASCADE)
|
|
|
|
|
recruitment_id = models.ForeignKey(Recruitment, on_delete=models.CASCADE)
|
|
|
|
|
sequence = models.IntegerField(default=0)
|
|
|
|
|
objects = HorillaCompanyManager(related_company_field="recruitment_ids__company_id")
|
|
|
|
|
|
2023-08-14 14:49:11 +05:30
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class RecruitmentSurveyAnswer(HorillaModel):
|
2023-08-14 14:49:11 +05:30
|
|
|
"""
|
|
|
|
|
RecruitmentSurveyAnswer
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
candidate_id = models.ForeignKey(Candidate, on_delete=models.CASCADE)
|
|
|
|
|
recruitment_id = models.ForeignKey(
|
2023-11-20 10:39:10 +05:30
|
|
|
Recruitment,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
verbose_name=_("Recruitment"),
|
|
|
|
|
null=True,
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
|
|
|
|
job_position_id = models.ForeignKey(
|
2023-11-20 10:39:10 +05:30
|
|
|
JobPosition,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
verbose_name=_("Job Position"),
|
|
|
|
|
null=True,
|
2023-08-14 14:49:11 +05:30
|
|
|
)
|
|
|
|
|
answer_json = models.JSONField()
|
|
|
|
|
attachment = models.FileField(
|
|
|
|
|
upload_to="recruitment_attachment", null=True, blank=True
|
|
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id")
|
2023-08-14 14:49:11 +05:30
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def answer(self):
|
|
|
|
|
"""
|
|
|
|
|
Used to convert the json to dict
|
|
|
|
|
"""
|
|
|
|
|
# Convert the JSON data to a dictionary
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(self.answer_json)
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
return {} # Return an empty dictionary if JSON is invalid or empty
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.candidate_id.name}-{self.recruitment_id}"
|
2023-12-21 17:15:25 +05:30
|
|
|
|
|
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class SkillZone(HorillaModel):
|
2024-01-23 15:29:05 +05:30
|
|
|
""" "
|
2024-01-09 15:56:58 +05:30
|
|
|
Model for talent pool
|
|
|
|
|
"""
|
2024-01-23 15:29:05 +05:30
|
|
|
|
2024-01-09 15:56:58 +05:30
|
|
|
title = models.CharField(max_length=50, verbose_name="Skill Zone")
|
2024-03-10 19:37:46 +05:30
|
|
|
description = models.TextField(verbose_name=_("Description"), max_length=255)
|
2024-03-13 11:29:18 +05:30
|
|
|
company_id = models.ForeignKey(
|
2024-03-27 14:20:20 +05:30
|
|
|
Company,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
verbose_name=_("Company"),
|
2024-03-13 11:29:18 +05:30
|
|
|
)
|
|
|
|
|
objects = HorillaCompanyManager()
|
2024-01-09 15:56:58 +05:30
|
|
|
|
2025-03-25 14:20:32 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Skill Zone")
|
|
|
|
|
verbose_name_plural = _("Skill Zones")
|
|
|
|
|
|
2024-01-09 15:56:58 +05:30
|
|
|
def get_active(self):
|
2024-01-23 15:29:05 +05:30
|
|
|
return SkillZoneCandidate.objects.filter(is_active=True, skill_zone_id=self)
|
|
|
|
|
|
2023-12-21 17:15:25 +05:30
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return self.title
|
2024-01-06 09:52:53 +05:30
|
|
|
|
2024-01-17 11:24:47 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class SkillZoneCandidate(HorillaModel):
|
2024-01-09 15:56:58 +05:30
|
|
|
"""
|
|
|
|
|
Model for saving candidate data's for future recruitment
|
|
|
|
|
"""
|
2024-01-23 15:29:05 +05:30
|
|
|
|
2024-01-09 15:56:58 +05:30
|
|
|
skill_zone_id = models.ForeignKey(
|
|
|
|
|
SkillZone,
|
|
|
|
|
verbose_name=_("Skill Zone"),
|
2024-01-23 15:29:05 +05:30
|
|
|
related_name="skillzonecandidate_set",
|
2024-01-12 21:35:41 +05:30
|
|
|
on_delete=models.PROTECT,
|
2024-01-23 15:29:05 +05:30
|
|
|
null=True,
|
2024-01-09 15:56:58 +05:30
|
|
|
)
|
|
|
|
|
candidate_id = models.ForeignKey(
|
|
|
|
|
Candidate,
|
2024-01-23 15:29:05 +05:30
|
|
|
on_delete=models.PROTECT,
|
2024-01-09 15:56:58 +05:30
|
|
|
null=True,
|
|
|
|
|
related_name="skillzonecandidate_set",
|
2024-01-23 15:29:05 +05:30
|
|
|
verbose_name=_("Candidate"),
|
2024-01-09 15:56:58 +05:30
|
|
|
)
|
|
|
|
|
# job_position_id=models.ForeignKey(
|
|
|
|
|
# JobPosition,
|
|
|
|
|
# on_delete=models.PROTECT,
|
|
|
|
|
# null=True,
|
|
|
|
|
# related_name="talent_pool",
|
|
|
|
|
# verbose_name=_("Job Position")
|
|
|
|
|
# )
|
|
|
|
|
|
2024-01-23 15:29:05 +05:30
|
|
|
reason = models.CharField(max_length=200, verbose_name=_("Reason"))
|
2024-02-14 15:05:00 +05:30
|
|
|
added_on = models.DateField(auto_now_add=True)
|
2024-03-27 14:20:20 +05:30
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="candidate_id__recruitment_id__company_id"
|
|
|
|
|
)
|
2024-01-09 15:56:58 +05:30
|
|
|
|
2024-12-05 17:18:10 +05:30
|
|
|
def clean(self):
|
|
|
|
|
# Check for duplicate entries in the database
|
|
|
|
|
duplicate_exists = (
|
|
|
|
|
SkillZoneCandidate.objects.filter(
|
|
|
|
|
candidate_id=self.candidate_id, skill_zone_id=self.skill_zone_id
|
|
|
|
|
)
|
|
|
|
|
.exclude(pk=self.pk)
|
|
|
|
|
.exists()
|
2024-01-09 15:56:58 +05:30
|
|
|
)
|
|
|
|
|
|
2024-12-05 17:18:10 +05:30
|
|
|
if duplicate_exists:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
_(
|
|
|
|
|
f"Candidate {self.candidate_id} already exists in Skill Zone {self.skill_zone_id}."
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
super().clean()
|
|
|
|
|
|
2024-01-09 15:56:58 +05:30
|
|
|
def __str__(self) -> str:
|
2024-03-10 19:37:46 +05:30
|
|
|
return str(self.candidate_id.get_full_name())
|
|
|
|
|
|
2024-01-09 15:56:58 +05:30
|
|
|
|
2024-03-27 14:20:20 +05:30
|
|
|
class CandidateRating(HorillaModel):
|
2024-01-23 15:29:05 +05:30
|
|
|
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)]
|
|
|
|
|
)
|
|
|
|
|
|
2024-01-06 09:52:53 +05:30
|
|
|
class Meta:
|
2024-01-23 15:29:05 +05:30
|
|
|
unique_together = ["employee_id", "candidate_id"]
|
2024-01-06 09:52:53 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2024-01-09 15:56:58 +05:30
|
|
|
return f"{self.employee_id} - {self.candidate_id} rating {self.rating}"
|
2024-01-31 11:42:29 +05:30
|
|
|
|
|
|
|
|
|
2024-04-15 13:37:52 +05:30
|
|
|
class RecruitmentGeneralSetting(HorillaModel):
|
2024-01-31 11:42:29 +05:30
|
|
|
"""
|
|
|
|
|
RecruitmentGeneralSettings model
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
candidate_self_tracking = models.BooleanField(default=False)
|
|
|
|
|
show_overall_rating = models.BooleanField(default=False)
|
2024-02-06 12:48:58 +05:30
|
|
|
company_id = models.ForeignKey(Company, on_delete=models.CASCADE, null=True)
|
2024-04-23 14:44:35 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class InterviewSchedule(HorillaModel):
|
|
|
|
|
"""
|
|
|
|
|
Interview Scheduling Model
|
|
|
|
|
"""
|
2024-05-07 12:23:36 +05:30
|
|
|
|
|
|
|
|
candidate_id = models.ForeignKey(
|
|
|
|
|
Candidate,
|
|
|
|
|
verbose_name=_("Candidate"),
|
|
|
|
|
related_name="candidate_interview",
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
2024-12-31 19:01:08 +05:30
|
|
|
employee_id = models.ManyToManyField(Employee, verbose_name=_("Interviewer"))
|
2024-04-23 14:44:35 +05:30
|
|
|
interview_date = models.DateField(verbose_name=_("Interview Date"))
|
|
|
|
|
interview_time = models.TimeField(verbose_name=_("Interview Time"))
|
2024-06-10 16:31:12 +05:30
|
|
|
description = models.TextField(
|
|
|
|
|
verbose_name=_("Description"), blank=True, max_length=255
|
|
|
|
|
)
|
2024-05-07 12:23:36 +05:30
|
|
|
completed = models.BooleanField(
|
|
|
|
|
default=False, verbose_name=_("Is Interview Completed")
|
|
|
|
|
)
|
2024-11-15 11:01:05 +05:30
|
|
|
objects = HorillaCompanyManager("candidate_id__recruitment_id__company_id")
|
2024-04-23 14:44:35 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.candidate_id} -Interview."
|
2024-07-08 14:54:48 +05:30
|
|
|
|
2025-05-28 14:30:01 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Schedule Interview")
|
|
|
|
|
verbose_name_plural = _("Schedule Interviews")
|
|
|
|
|
|
2024-07-08 14:54:48 +05:30
|
|
|
|
|
|
|
|
class Resume(models.Model):
|
|
|
|
|
file = models.FileField(
|
|
|
|
|
upload_to="recruitment/resume",
|
|
|
|
|
validators=[
|
|
|
|
|
validate_pdf,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
recruitment_id = models.ForeignKey(
|
|
|
|
|
Recruitment, on_delete=models.CASCADE, related_name="resume"
|
|
|
|
|
)
|
|
|
|
|
is_candidate = models.BooleanField(default=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.recruitment_id} - Resume {self.pk}"
|
2024-11-29 12:14:44 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
STATUS = [
|
|
|
|
|
("requested", "Requested"),
|
|
|
|
|
("approved", "Approved"),
|
|
|
|
|
("rejected", "Rejected"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
FORMATS = [
|
|
|
|
|
("any", "Any"),
|
|
|
|
|
("pdf", "PDF"),
|
|
|
|
|
("txt", "TXT"),
|
|
|
|
|
("docx", "DOCX"),
|
|
|
|
|
("xlsx", "XLSX"),
|
|
|
|
|
("jpg", "JPG"),
|
|
|
|
|
("png", "PNG"),
|
|
|
|
|
("jpeg", "JPEG"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CandidateDocumentRequest(HorillaModel):
|
|
|
|
|
title = models.CharField(max_length=100)
|
|
|
|
|
candidate_id = models.ManyToManyField(Candidate)
|
|
|
|
|
format = models.CharField(choices=FORMATS, max_length=10)
|
|
|
|
|
max_size = models.IntegerField(blank=True, null=True)
|
|
|
|
|
description = models.TextField(blank=True, null=True, max_length=255)
|
|
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="employee_id__employee_work_info__company_id"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CandidateDocument(HorillaModel):
|
|
|
|
|
title = models.CharField(max_length=250)
|
2024-12-02 16:48:26 +05:30
|
|
|
candidate_id = models.ForeignKey(
|
|
|
|
|
Candidate, on_delete=models.PROTECT, verbose_name="Candidate"
|
|
|
|
|
)
|
2024-11-29 12:14:44 +05:30
|
|
|
document_request_id = models.ForeignKey(
|
|
|
|
|
CandidateDocumentRequest, on_delete=models.PROTECT, null=True
|
|
|
|
|
)
|
|
|
|
|
document = models.FileField(upload_to="candidate/documents", null=True)
|
|
|
|
|
status = models.CharField(choices=STATUS, max_length=10, default="requested")
|
|
|
|
|
reject_reason = models.TextField(blank=True, null=True, max_length=255)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.candidate_id} - {self.title}"
|
|
|
|
|
|
|
|
|
|
def clean(self, *args, **kwargs):
|
|
|
|
|
super().clean(*args, **kwargs)
|
|
|
|
|
file = self.document
|
|
|
|
|
|
|
|
|
|
if len(self.title) < 3:
|
|
|
|
|
raise ValidationError({"title": _("Title must be at least 3 characters")})
|
|
|
|
|
|
|
|
|
|
if file and self.document_request_id:
|
|
|
|
|
format = self.document_request_id.format
|
|
|
|
|
max_size = self.document_request_id.max_size
|
|
|
|
|
if max_size:
|
|
|
|
|
if file.size > max_size * 1024 * 1024:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
{"document": _("File size exceeds the limit")}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ext = file.name.split(".")[1].lower()
|
|
|
|
|
if format == "any":
|
|
|
|
|
pass
|
|
|
|
|
elif ext != format:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
{"document": _("Please upload {} file only.").format(format)}
|
|
|
|
|
)
|
2025-05-08 13:18:49 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class LinkedInAccount(HorillaModel):
|
|
|
|
|
username = models.CharField(max_length=250, verbose_name="Username")
|
|
|
|
|
email = models.EmailField(max_length=254, verbose_name=_("Email"))
|
|
|
|
|
api_token = models.CharField(max_length=500, verbose_name="API Token")
|
|
|
|
|
sub_id = models.CharField(max_length=250, unique=True)
|
|
|
|
|
company_id = models.ForeignKey(Company, on_delete=models.CASCADE, null=True)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return str(self.username)
|
|
|
|
|
|
|
|
|
|
def clean(self, *args, **kwargs):
|
|
|
|
|
super().clean(*args, **kwargs)
|
|
|
|
|
url = "https://api.linkedin.com/v2/userinfo"
|
|
|
|
|
headers = {"Authorization": f"Bearer {self.api_token}"}
|
|
|
|
|
|
|
|
|
|
response = requests.get(url, headers=headers)
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
data = response.json()
|
|
|
|
|
if not data["email"] == self.email:
|
|
|
|
|
raise ValidationError({"email": _("Email mismatched.")})
|
|
|
|
|
self.sub_id = response.json()["sub"]
|
|
|
|
|
else:
|
|
|
|
|
raise ValidationError(_("Check the credentials"))
|
|
|
|
|
|
|
|
|
|
def action_template(self):
|
|
|
|
|
"""
|
|
|
|
|
This method for get custom column for managers.
|
|
|
|
|
"""
|
|
|
|
|
return render_template(
|
|
|
|
|
path="linkedin/linkedin_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def is_active_toggle(self):
|
|
|
|
|
"""
|
|
|
|
|
For toggle is_active field
|
|
|
|
|
"""
|
|
|
|
|
url = f"update-isactive-linkedin-account/{self.id}"
|
|
|
|
|
return render_template(
|
|
|
|
|
path="is_active_toggle.html",
|
|
|
|
|
context={"instance": self, "url": url},
|
|
|
|
|
)
|