""" models.py This module is used to register models for recruitment app """ import json import os import re from uuid import uuid4 import django import requests from django import forms from django.core.exceptions import ValidationError from django.core.files.storage import default_storage from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company, JobPosition from employee.models import Employee from horilla.models import HorillaModel, upload_path from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog from horilla_views.cbv_methods import render_template # Create your models here. 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." ) raise forms.ValidationError( "Invalid input: Only digits and spaces are allowed." ) def validate_pdf(value): """ This method is used to validate pdf """ ext = os.path.splitext(value.name)[1] # Get file extension if ext.lower() != ".pdf": raise ValidationError(_("File must be a PDF.")) def validate_image(value): """ This method is used to validate the image """ return value 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) class SurveyTemplate(HorillaModel): """ SurveyTemplate Model """ title = models.CharField(max_length=50, unique=True) description = models.TextField(null=True, blank=True) is_general_template = models.BooleanField(default=False, editable=False) company_id = models.ForeignKey( Company, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Company"), ) objects = HorillaCompanyManager("company_id") def __str__(self) -> str: return self.title class Meta: verbose_name = _("Survey Template") verbose_name_plural = _("Survey Templates") 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) class Meta: verbose_name = _("Skill") verbose_name_plural = _("Skills") class Recruitment(HorillaModel): """ Recruitment model """ title = models.CharField( max_length=50, null=True, blank=True, verbose_name=_("Title") ) description = models.TextField(null=True, verbose_name=_("Description")) is_event_based = models.BooleanField( default=False, help_text=_("To start recruitment for multiple job positions"), ) closed = models.BooleanField( default=False, help_text=_( "To close the recruitment, If closed then not visible on pipeline view." ), verbose_name=_("Closed"), ) 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." ), verbose_name=_("Is Published"), ) open_positions = models.ManyToManyField( JobPosition, related_name="open_positions", blank=True, verbose_name=_("Job Position"), ) job_position_id = models.ForeignKey( JobPosition, on_delete=models.PROTECT, null=True, blank=True, db_constraint=False, related_name="recruitment", verbose_name=_("Job Position"), editable=False, ) 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") ) company_id = models.ForeignKey( Company, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Company"), ) start_date = models.DateField( default=django.utils.timezone.now, verbose_name=_("Start Date") ) end_date = models.DateField(blank=True, null=True, verbose_name=_("End Date")) skills = models.ManyToManyField(Skill, blank=True, verbose_name=_("Skills")) 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." ), verbose_name=_("Post on LinkedIn"), ) objects = HorillaCompanyManager() default = models.manager.Manager() optional_profile_image = models.BooleanField( default=False, help_text=_("Profile image not mandatory for candidate creation"), verbose_name=_("Optional Profile Image"), ) optional_resume = models.BooleanField( default=False, help_text=_("Resume not mandatory for candidate creation"), verbose_name=_("Optional Resume"), ) class Meta: """ 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"),) verbose_name = _("Recruitment") verbose_name_plural = _("Recruitments") def total_hires(self): """ This method is used to get the count of hired candidates """ return self.candidate.filter(hired=True).count() def __str__(self): 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: raise ValidationError({"title": _("This field is required")}) if self.is_published: if self.vacancy <= 0: raise ValidationError( _( "Vacancy must be greater than zero if the recruitment is publishing." ) ) if self.end_date is not None and ( self.start_date is not None and self.start_date > self.end_date ): raise ValidationError( {"end_date": _("End date cannot be less than start date.")} ) return super().clean() def save(self, *args, **kwargs): if not self.publish_in_linkedin: self.linkedin_account_id = None self.linkedin_post_id = None super().save(*args, **kwargs) # Save the Recruitment instance first if self.is_event_based and self.open_positions is None: raise ValidationError({"open_positions": _("This field is required")}) def ordered_stages(self): """ This method will returns all the stage respectively to the ascending order of stages """ return self.stage_set.order_by("sequence") 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 class Stage(HorillaModel): """ Stage model """ stage_types = [ ("initial", _("Initial")), ("applied", _("Applied")), ("test", _("Test")), ("interview", _("Interview")), ("cancelled", _("Cancelled")), ("hired", _("Hired")), ] recruitment_id = models.ForeignKey( Recruitment, on_delete=models.CASCADE, related_name="stage_set", verbose_name=_("Recruitment"), ) stage_managers = models.ManyToManyField(Employee, verbose_name=_("Stage Managers")) stage = models.CharField(max_length=50, verbose_name=_("Stage")) stage_type = models.CharField( max_length=20, choices=stage_types, default="interview", verbose_name=_("Stage Type"), ) sequence = models.IntegerField(null=True, default=0) objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id") def __str__(self): return f"{self.stage}" class Meta: """ Meta class to add the additional info """ permissions = (("archive_Stage", "Archive Stage"),) unique_together = ["recruitment_id", "stage"] ordering = ["sequence"] verbose_name = _("Stage") verbose_name_plural = _("Stages") def __str__(self): return f"{self.stage} - ({self.recruitment_id.title})" 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 ) } def candidate_upload_path(instance, filename): """ Generates a unique file path for candidate profile & resume uploads. """ ext = filename.split(".")[-1] name_slug = slugify(instance.name) or "candidate" unique_filename = f"{name_slug}-{uuid4().hex[:8]}.{ext}" return f"recruitment/{name_slug}/{unique_filename}" class Candidate(HorillaModel): """ Candidate model """ choices = [("male", _("Male")), ("female", _("Female")), ("other", _("Other"))] offer_letter_statuses = [ ("not_sent", _("Not Sent")), ("sent", _("Sent")), ("accepted", _("Accepted")), ("rejected", _("Rejected")), ("joined", _("Joined")), ] 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=upload_path, null=True) # 853 portfolio = models.URLField(max_length=200, blank=True) recruitment_id = models.ForeignKey( Recruitment, on_delete=models.PROTECT, null=True, related_name="candidate", verbose_name=_("Recruitment"), ) job_position_id = models.ForeignKey( JobPosition, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Job Position"), ) stage_id = models.ForeignKey( Stage, on_delete=models.PROTECT, null=True, verbose_name=_("Stage"), ) converted_employee_id = models.ForeignKey( Employee, on_delete=models.SET_NULL, blank=True, null=True, related_name="candidate_get", verbose_name=_("Employee"), ) schedule_date = models.DateTimeField( blank=True, null=True, verbose_name=_("Schedule date") ) email = models.EmailField(max_length=254, unique=True, verbose_name=_("Email")) mobile = models.CharField( max_length=15, blank=True, validators=[ validate_mobile, ], verbose_name=_("Mobile"), ) resume = models.FileField( upload_to=upload_path, # 853 validators=[ validate_pdf, ], ) referral = models.ForeignKey( Employee, on_delete=models.CASCADE, null=True, blank=True, related_name="candidate_referral", verbose_name=_("Referral"), ) address = models.TextField( null=True, blank=True, verbose_name=_("Address"), max_length=255 ) 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( max_length=15, choices=choices, null=True, default="male", verbose_name=_("Gender"), ) source = models.CharField( 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")) canceled = models.BooleanField(default=False, verbose_name=_("Canceled")) converted = models.BooleanField(default=False, verbose_name=_("Converted")) joining_date = models.DateField( blank=True, null=True, verbose_name=_("Joining Date") ) history = HorillaAuditLog( related_name="history_set", bases=[ HorillaAuditInfo, ], ) sequence = models.IntegerField(null=True, default=0) probation_end = models.DateField(null=True, editable=False) offer_letter_status = models.CharField( max_length=10, choices=offer_letter_statuses, default="not_sent", editable=False, verbose_name=_("Offer Letter Status"), ) objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id") last_updated = models.DateField(null=True, auto_now=True) 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"), ] hired_date = models.DateField(null=True, blank=True, editable=False) def __str__(self): return f"{self.name}" 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 def get_full_name(self): """ Method will return employee full name """ return str(self.name) def get_avatar(self): """ Method will rerun the api to the avatar or path to the profile image """ url = ( f"https://ui-avatars.com/api/?name={self.get_full_name()}&background=random" ) if self.profile: full_filename = self.profile.name if default_storage.exists(full_filename): url = self.profile.url return url 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 def get_mail(self): """ """ return self.get_email() def phone(self): return self.mobile def tracking(self): """ This method is used to return the tracked history of the instance """ return get_diff(self) def get_last_sent_mail(self): """ This method is used to get last send mail """ from base.models import EmailLog return ( EmailLog.objects.filter(to__icontains=self.email) .order_by("-created_at") .first() ) 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 = "
| Sl No. | Date | Time | Is Completed |
|---|---|---|---|
| {index} | " interview_info += ( f"{interview.interview_date} | " ) interview_info += ( f"{interview.interview_time} | " ) interview_info += ( f"{'Yes' if interview.completed else 'No'} |