2024-08-27 17:34:55 +05:30
|
|
|
import operator
|
2025-06-11 14:36:06 +05:30
|
|
|
import re
|
|
|
|
|
from datetime import date, datetime, timezone
|
2024-08-27 17:34:55 +05:30
|
|
|
|
2024-05-07 12:23:36 +05:30
|
|
|
from dateutil.relativedelta import relativedelta
|
2024-08-27 17:34:55 +05:30
|
|
|
from django.apps import apps
|
2024-03-08 22:35:28 +05:30
|
|
|
from django.core.exceptions import ValidationError
|
2024-04-11 12:34:55 +05:30
|
|
|
from django.core.validators import MinValueValidator
|
2024-05-07 12:23:36 +05:30
|
|
|
from django.db import models
|
2025-06-11 14:36:06 +05:30
|
|
|
from django.db.models import Value
|
|
|
|
|
from django.db.models.functions import Concat
|
|
|
|
|
from django.urls import reverse, reverse_lazy
|
|
|
|
|
from django.utils import timezone
|
2024-05-07 12:23:36 +05:30
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
2023-12-01 15:36:51 +05:30
|
|
|
from base.horilla_company_manager import HorillaCompanyManager
|
2024-05-07 12:23:36 +05:30
|
|
|
from base.models import Company, Department, JobPosition
|
2024-09-12 14:11:50 +05:30
|
|
|
from employee.models import BonusPoint, Employee
|
2025-06-11 14:36:06 +05:30
|
|
|
from horilla.horilla_middlewares import _thread_locals
|
2024-05-07 12:23:36 +05:30
|
|
|
from horilla.models import HorillaModel
|
|
|
|
|
from horilla_audit.methods import get_diff
|
|
|
|
|
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
|
2024-08-27 17:34:55 +05:30
|
|
|
from horilla_views.cbv_methods import render_template
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
"""Objectives and key result section"""
|
|
|
|
|
|
|
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class Period(HorillaModel):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""this is a period model used for creating period"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-09-09 14:10:09 +05:30
|
|
|
period_name = models.CharField(max_length=150, unique=True)
|
2023-09-25 15:53:42 +05:30
|
|
|
start_date = models.DateField()
|
|
|
|
|
end_date = models.DateField()
|
2023-12-01 15:36:51 +05:30
|
|
|
company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company"))
|
2024-11-15 11:00:12 +05:30
|
|
|
objects = HorillaCompanyManager("company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.period_name
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/period/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def detail_view(self):
|
|
|
|
|
"""
|
|
|
|
|
detail view
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse("period-detail-view", kwargs={"pk": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def detail_view_actions(self):
|
|
|
|
|
"""
|
|
|
|
|
detail view actions
|
|
|
|
|
"""
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/period/detail_view_actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def company_id_detail(self):
|
|
|
|
|
"""
|
|
|
|
|
interviewer in detail view
|
|
|
|
|
"""
|
|
|
|
|
company_name = self.company_id.all()
|
|
|
|
|
company_names_string = ", ".join([str(company) for company in company_name])
|
|
|
|
|
return company_names_string
|
|
|
|
|
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class KeyResult(HorillaModel):
|
2024-03-08 22:35:28 +05:30
|
|
|
"""model used to create key results"""
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
PROGRESS_CHOICES = (
|
|
|
|
|
("%", _("Percentage")),
|
|
|
|
|
("#", _("Number")),
|
|
|
|
|
("Currency", (("$", "USD$"), ("₹", "INR"), ("€", "EUR"))),
|
|
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
title = models.CharField(
|
|
|
|
|
max_length=60, null=True, blank=False, verbose_name="Title"
|
|
|
|
|
)
|
|
|
|
|
description = models.TextField(
|
|
|
|
|
blank=False, null=False, max_length=255, verbose_name="Description"
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
progress_type = models.CharField(
|
2024-03-28 14:28:18 +05:30
|
|
|
max_length=60, default="%", choices=PROGRESS_CHOICES
|
2024-03-08 22:35:28 +05:30
|
|
|
)
|
2024-03-26 14:11:47 +05:30
|
|
|
target_value = models.IntegerField(null=True, blank=True, default=100)
|
2025-06-11 14:36:06 +05:30
|
|
|
duration = models.IntegerField(null=True, blank=True, help_text="In Days")
|
2024-07-17 14:09:12 +05:30
|
|
|
archive = models.BooleanField(default=False)
|
2024-03-08 22:35:28 +05:30
|
|
|
history = HorillaAuditLog(bases=[HorillaAuditInfo])
|
2024-03-18 16:44:13 +05:30
|
|
|
company_id = models.ForeignKey(
|
|
|
|
|
Company,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Company"),
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
objects = HorillaCompanyManager()
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-04-04 17:11:53 +05:30
|
|
|
class Meta:
|
|
|
|
|
"""
|
|
|
|
|
Meta class for additional options
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
ordering = [
|
|
|
|
|
"-id",
|
|
|
|
|
]
|
|
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.title}"
|
|
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def get_progress_type(self):
|
|
|
|
|
currency_dict = dict(self.PROGRESS_CHOICES[2][1])
|
|
|
|
|
if self.progress_type in currency_dict:
|
|
|
|
|
return currency_dict[self.progress_type]
|
|
|
|
|
progress_dict = dict(self.PROGRESS_CHOICES)
|
|
|
|
|
return progress_dict.get(self.progress_type)
|
|
|
|
|
|
|
|
|
|
def action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
This method for get custome coloumn .
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/key_results/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def detail_action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
This method for get custome coloumn .
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/key_results/detail_view_actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_avatar(self):
|
|
|
|
|
"""
|
|
|
|
|
Method will return the API URL for the avatar or the path to the profile image.
|
|
|
|
|
"""
|
|
|
|
|
sanitized_title = re.sub(r"[^a-zA-Z0-9\s]", "", self.title)
|
|
|
|
|
sanitized_title = sanitized_title.replace(" ", "+")
|
|
|
|
|
url = f"https://ui-avatars.com/api/?name={sanitized_title}&background=random"
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_delete_url(self):
|
|
|
|
|
"""
|
|
|
|
|
to get the delete url for card action delete
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse("delete-key-result", kwargs={"obj_id": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_detail_url(self):
|
|
|
|
|
"""
|
|
|
|
|
Detail view url
|
|
|
|
|
"""
|
|
|
|
|
url = reverse_lazy("key-result-detail-view", kwargs={"pk": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_update_url(self):
|
|
|
|
|
"""
|
|
|
|
|
to get the update url for card action update
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse("update-key-result", kwargs={"pk": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class Objective(HorillaModel):
|
2024-03-10 19:37:46 +05:30
|
|
|
"""Model used for creating objectives"""
|
|
|
|
|
|
2024-05-22 10:33:48 +05:30
|
|
|
DURATION_UNIT = (
|
|
|
|
|
("days", _("Days")),
|
|
|
|
|
("months", _("Months")),
|
|
|
|
|
("years", _("Years")),
|
|
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
title = models.CharField(
|
|
|
|
|
null=False, blank=False, max_length=100, verbose_name="Title"
|
|
|
|
|
)
|
|
|
|
|
description = models.TextField(
|
|
|
|
|
blank=False, null=False, max_length=255, verbose_name="Description"
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
managers = models.ManyToManyField(
|
2024-03-10 19:37:46 +05:30
|
|
|
Employee, related_name="objective", blank=True, verbose_name="Managers"
|
2024-03-08 22:35:28 +05:30
|
|
|
)
|
|
|
|
|
assignees = models.ManyToManyField(
|
|
|
|
|
Employee,
|
|
|
|
|
related_name="assignees_objective",
|
2024-03-10 19:37:46 +05:30
|
|
|
blank=True,
|
|
|
|
|
verbose_name="Assignees",
|
2024-03-08 22:35:28 +05:30
|
|
|
)
|
|
|
|
|
key_result_id = models.ManyToManyField(
|
|
|
|
|
KeyResult,
|
2024-03-10 19:37:46 +05:30
|
|
|
blank=True,
|
|
|
|
|
related_name="objective",
|
2024-03-26 14:11:47 +05:30
|
|
|
verbose_name="Default Key results",
|
2024-03-10 19:37:46 +05:30
|
|
|
)
|
2024-05-22 10:33:48 +05:30
|
|
|
duration_unit = models.CharField(
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=DURATION_UNIT,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
default="days",
|
|
|
|
|
verbose_name="Duration Unit",
|
|
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
duration = models.IntegerField(default=1, validators=[MinValueValidator(0)])
|
|
|
|
|
add_assignees = models.BooleanField(default=False)
|
2024-07-17 14:09:12 +05:30
|
|
|
archive = models.BooleanField(default=False)
|
2024-03-08 22:35:28 +05:30
|
|
|
history = HorillaAuditLog(bases=[HorillaAuditInfo])
|
2024-03-18 16:44:13 +05:30
|
|
|
company_id = models.ForeignKey(
|
|
|
|
|
Company,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Company"),
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
2025-04-28 09:20:39 +05:30
|
|
|
self_employee_progress_update = models.BooleanField(default=True)
|
2025-08-01 17:58:33 +05:30
|
|
|
objects = HorillaCompanyManager()
|
2024-03-08 22:35:28 +05:30
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
"""
|
|
|
|
|
Meta class for additional options
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
ordering = [
|
|
|
|
|
"-id",
|
|
|
|
|
]
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.title}"
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def get_instance_id(self):
|
|
|
|
|
return self.pk
|
|
|
|
|
|
|
|
|
|
def title_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For title column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/title.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def manager_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For manager column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/manager.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def actions_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def self_action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For self action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/self_objective_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def key_res_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For Key results column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/key_results.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def self_key_res_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For Key results column for employee objectives
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/self_key_results.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def assingnees_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For Key results column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/assignees.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def duration_col(self):
|
|
|
|
|
"""
|
|
|
|
|
Duration col
|
|
|
|
|
"""
|
|
|
|
|
return (
|
|
|
|
|
str(self.duration) + " " + dict(self.DURATION_UNIT).get(self.duration_unit)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_employee_objective(self):
|
|
|
|
|
|
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
|
|
|
user = request.user.employee_get
|
|
|
|
|
emp_object = self.employee_objective.get(employee_id=user, objective_id=self.id)
|
|
|
|
|
return emp_object
|
|
|
|
|
|
|
|
|
|
def get_individual_url(self):
|
|
|
|
|
"""
|
|
|
|
|
Detail view of employee objective
|
|
|
|
|
"""
|
|
|
|
|
url = reverse_lazy("objective-detailed-view", kwargs={"obj_id": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
2025-08-01 17:58:33 +05:30
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
|
|
|
selected_company = request.session.get("selected_company")
|
|
|
|
|
if (
|
|
|
|
|
not self.id
|
|
|
|
|
and not self.company_id
|
|
|
|
|
and selected_company
|
|
|
|
|
and selected_company != "all"
|
|
|
|
|
):
|
|
|
|
|
self.company_id = Company.find(selected_company)
|
|
|
|
|
|
|
|
|
|
super().save()
|
|
|
|
|
|
2023-09-09 14:10:09 +05:30
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class EmployeeObjective(HorillaModel):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""this is a EmployObjective model used for creating Employee objectives"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2023-07-26 12:15:01 +05:30
|
|
|
STATUS_CHOICES = (
|
2025-06-11 14:36:06 +05:30
|
|
|
("Not Started", _("Not Started")),
|
2023-09-09 14:10:09 +05:30
|
|
|
("On Track", _("On Track")),
|
|
|
|
|
("Behind", _("Behind")),
|
|
|
|
|
("At Risk", _("At Risk")),
|
2025-06-11 14:36:06 +05:30
|
|
|
("Closed", _("Closed")),
|
2023-07-26 12:15:01 +05:30
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
objective = models.CharField(
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
max_length=100,
|
|
|
|
|
verbose_name="Title",
|
|
|
|
|
)
|
|
|
|
|
objective_description = models.TextField(
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
max_length=255,
|
|
|
|
|
verbose_name="Description",
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
created_at = models.DateField(auto_now_add=True)
|
2024-03-08 22:35:28 +05:30
|
|
|
objective_id = models.ForeignKey(
|
|
|
|
|
Objective,
|
2024-03-10 19:37:46 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
related_name="employee_objective",
|
|
|
|
|
verbose_name="Objective",
|
2024-03-08 22:35:28 +05:30
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
)
|
2023-09-09 14:10:09 +05:30
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
2024-03-10 19:37:46 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
2023-09-09 14:10:09 +05:30
|
|
|
related_name="employee_objective",
|
2024-03-08 22:35:28 +05:30
|
|
|
on_delete=models.PROTECT,
|
2024-03-10 19:37:46 +05:30
|
|
|
verbose_name="Employee",
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
key_result_id = models.ManyToManyField(
|
|
|
|
|
KeyResult,
|
|
|
|
|
blank=True,
|
2024-03-10 19:37:46 +05:30
|
|
|
related_name="employee_objective",
|
|
|
|
|
verbose_name="Key results",
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
updated_at = models.DateField(auto_now=True)
|
|
|
|
|
start_date = models.DateField(null=False, blank=False)
|
|
|
|
|
end_date = models.DateField(null=False, blank=False)
|
2023-09-09 14:10:09 +05:30
|
|
|
status = models.CharField(
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=STATUS_CHOICES,
|
|
|
|
|
null=False,
|
|
|
|
|
blank=False,
|
|
|
|
|
default="Not Started",
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
progress_percentage = models.IntegerField(default=0)
|
|
|
|
|
|
|
|
|
|
history = HorillaAuditLog(bases=[HorillaAuditInfo], related_name="history_set")
|
|
|
|
|
archive = models.BooleanField(default=False)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-07-17 14:09:12 +05:30
|
|
|
class Meta:
|
|
|
|
|
"""
|
|
|
|
|
Meta class for additional options
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
unique_together = ("employee_id", "objective_id")
|
|
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def update_objective_progress(self):
|
|
|
|
|
"""
|
|
|
|
|
used for updating progress percentage when current value of key result change
|
|
|
|
|
"""
|
|
|
|
|
krs = self.employee_key_result.all()
|
2024-03-10 19:37:46 +05:30
|
|
|
if len(krs) > 0:
|
|
|
|
|
current = 0
|
2024-03-08 22:35:28 +05:30
|
|
|
for kr in krs:
|
2025-06-11 14:36:06 +05:30
|
|
|
current += min(kr.progress_percentage, 100)
|
2024-03-10 19:37:46 +05:30
|
|
|
self.progress_percentage = int(current / len(krs))
|
2024-03-08 22:35:28 +05:30
|
|
|
self.save()
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.objective_id} | {self.employee_id}"
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
if not self.pk and self.objective_id and self.start_date:
|
|
|
|
|
duration = self.objective_id.duration
|
2024-05-22 10:33:48 +05:30
|
|
|
if self.objective_id.duration_unit == "days":
|
|
|
|
|
self.end_date = self.start_date + relativedelta(days=duration)
|
|
|
|
|
elif self.objective_id.duration_unit == "months":
|
|
|
|
|
self.end_date = self.start_date + relativedelta(months=duration)
|
|
|
|
|
elif self.objective_id.duration_unit == "years":
|
|
|
|
|
self.end_date = self.start_date + relativedelta(years=duration)
|
2024-07-17 14:09:12 +05:30
|
|
|
# Add assignees to the objective
|
|
|
|
|
objective = self.objective_id
|
|
|
|
|
if self.employee_id not in objective.assignees.all():
|
|
|
|
|
objective.assignees.add(self.employee_id)
|
2024-03-08 22:35:28 +05:30
|
|
|
super().save(*args, **kwargs)
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def tracking(self):
|
2024-03-10 19:37:46 +05:30
|
|
|
return get_diff(self)
|
|
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def employee_objective_detail_view(self):
|
|
|
|
|
"""
|
|
|
|
|
for detail view of page
|
|
|
|
|
"""
|
2025-08-01 17:58:33 +05:30
|
|
|
url = reverse("view-employee-objective", kwargs={"emp_obj_id": self.pk})
|
2025-06-11 14:36:06 +05:30
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def title_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For title column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/title.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def emp_obj_action(self):
|
|
|
|
|
"""
|
|
|
|
|
Action in detail view
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/emp_obj_actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def status_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For status column
|
|
|
|
|
"""
|
|
|
|
|
objective_key_result_status = self.STATUS_CHOICES
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/employee_objective_status.html",
|
|
|
|
|
context={
|
|
|
|
|
"instance": self,
|
|
|
|
|
"objective_key_result_status": objective_key_result_status,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def objective_detail_subtitle(self):
|
|
|
|
|
"""
|
|
|
|
|
Return subtitle containing both department and job position information.
|
|
|
|
|
"""
|
|
|
|
|
return f"{self.employee_id.employee_work_info.department_id} / {self.employee_id.employee_work_info.job_position_id}"
|
|
|
|
|
|
|
|
|
|
def manager_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For manager column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/manager.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def actions_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def self_action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For self action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/self_objective_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def key_res_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For Key results column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/key_results.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def assingnees_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For Key results column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/objectives/assignees.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def duration_col(self):
|
|
|
|
|
"""
|
|
|
|
|
Duration col
|
|
|
|
|
"""
|
|
|
|
|
return (
|
|
|
|
|
str(self.objective_id.duration)
|
|
|
|
|
+ " "
|
|
|
|
|
+ dict(self.objective_id.DURATION_UNIT).get(self.objective_id.duration_unit)
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
class Comment(models.Model):
|
|
|
|
|
"""comments for objectives"""
|
|
|
|
|
|
2025-07-03 16:32:24 +05:30
|
|
|
comment = models.TextField()
|
2023-09-09 14:10:09 +05:30
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="comment",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
employee_objective_id = models.ForeignKey(
|
|
|
|
|
EmployeeObjective,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="emp_objective",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
2023-10-26 12:54:15 +05:30
|
|
|
history = HorillaAuditLog(excluded_fields=["comment"], bases=[HorillaAuditInfo])
|
2024-03-28 14:28:18 +05:30
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="employee_id__employee_work_info__company_id"
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-11-20 14:30:32 +05:30
|
|
|
return f"{self.employee_id.employee_first_name} - {self.comment} "
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-04-24 12:16:28 +05:30
|
|
|
class EmployeeKeyResult(models.Model):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""employee key result creation"""
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
PROGRESS_CHOICES = (
|
2023-09-09 14:10:09 +05:30
|
|
|
("%", _("Percentage")),
|
|
|
|
|
("#", _("Number")),
|
|
|
|
|
("Currency", (("$", "USD$"), ("₹", "INR"), ("€", "EUR"))),
|
2023-05-10 15:06:57 +05:30
|
|
|
)
|
|
|
|
|
STATUS_CHOICES = (
|
2025-06-11 14:36:06 +05:30
|
|
|
("Not Started", _("Not Started")),
|
2023-09-09 14:10:09 +05:30
|
|
|
("On Track", _("On Track")),
|
|
|
|
|
("Behind", _("Behind")),
|
|
|
|
|
("At Risk", _("At Risk")),
|
2025-06-11 14:36:06 +05:30
|
|
|
("Closed", _("Closed")),
|
2023-05-10 15:06:57 +05:30
|
|
|
)
|
|
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
key_result = models.CharField(max_length=60, null=True, blank=True)
|
2024-03-10 19:37:46 +05:30
|
|
|
key_result_description = models.TextField(blank=True, null=True, max_length=255)
|
|
|
|
|
employee_objective_id = models.ForeignKey(
|
2024-03-08 22:35:28 +05:30
|
|
|
EmployeeObjective,
|
2024-03-10 19:37:46 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
related_name="employee_key_result",
|
2024-03-08 22:35:28 +05:30
|
|
|
on_delete=models.CASCADE,
|
2024-03-10 19:37:46 +05:30
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
key_result_id = models.ForeignKey(
|
|
|
|
|
KeyResult,
|
2024-03-10 19:37:46 +05:30
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
related_name="employee_key_result",
|
|
|
|
|
verbose_name="Key result",
|
2024-03-08 22:35:28 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
|
|
|
|
progress_type = models.CharField(
|
|
|
|
|
max_length=60, null=True, blank=True, choices=PROGRESS_CHOICES
|
|
|
|
|
)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=STATUS_CHOICES,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
default="Not Started",
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
created_at = models.DateField(auto_now_add=True, blank=True, null=True)
|
|
|
|
|
updated_at = models.DateField(auto_now=True, null=True, blank=True)
|
|
|
|
|
start_value = models.IntegerField(null=True, blank=True, default=0)
|
|
|
|
|
current_value = models.IntegerField(null=True, blank=True, default=0)
|
|
|
|
|
target_value = models.IntegerField(null=True, blank=True, default=0)
|
|
|
|
|
start_date = models.DateField(null=True, blank=True)
|
|
|
|
|
end_date = models.DateField(null=True, blank=True)
|
2023-10-26 12:54:15 +05:30
|
|
|
history = HorillaAuditLog(bases=[HorillaAuditInfo])
|
2024-03-28 14:28:18 +05:30
|
|
|
objects = HorillaCompanyManager(
|
|
|
|
|
related_company_field="employee_objective_id__objective_id__company_id"
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
progress_percentage = models.IntegerField(default=0)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
2025-06-11 14:36:06 +05:30
|
|
|
return f"{self.key_result_id} | {self.employee_objective_id.employee_id}"
|
|
|
|
|
|
|
|
|
|
def get_update_url(self):
|
|
|
|
|
"""
|
|
|
|
|
to get the update url for card action update
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse("employee-key-result-update", kwargs={"pk": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_delete_url(self):
|
|
|
|
|
"""
|
|
|
|
|
to get the delete url for card action delete
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse("delete-employee-keyresult", kwargs={"kr_id": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def key_result_column(self):
|
|
|
|
|
|
|
|
|
|
today = datetime.today().date()
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/dashboard/keyresult_col.html",
|
|
|
|
|
context={"instance": self, "today": today},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def actions_col(self):
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/dashboard/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def title_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For title column
|
|
|
|
|
"""
|
|
|
|
|
due = None
|
|
|
|
|
color = "success"
|
|
|
|
|
if self.end_date:
|
|
|
|
|
due = (
|
|
|
|
|
f"due {self.end_date}"
|
|
|
|
|
if self.end_date == date.today()
|
|
|
|
|
else f"due in{self.end_date - date.today()}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if self.end_date < date.today():
|
|
|
|
|
color = "danger"
|
|
|
|
|
elif self.end_date == date.today():
|
|
|
|
|
color = "warning"
|
|
|
|
|
|
|
|
|
|
col = f"""
|
|
|
|
|
<span class='d-flex justify-content-between align-items-center'
|
|
|
|
|
>
|
|
|
|
|
{self.key_result}
|
|
|
|
|
<span title = 'due {due}'>
|
|
|
|
|
<ion-icon
|
|
|
|
|
class="text-{color}"
|
|
|
|
|
name="time-sharp"
|
|
|
|
|
>
|
|
|
|
|
</ion-icon>
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return col
|
|
|
|
|
|
|
|
|
|
def get_current_value_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For current value column
|
|
|
|
|
"""
|
|
|
|
|
# request is required
|
|
|
|
|
request = _thread_locals.request
|
|
|
|
|
if (
|
|
|
|
|
request.user.has_perm("pms.change_objective")
|
|
|
|
|
or request.user.has_perm("pms.change_employeeobjective")
|
|
|
|
|
or request.user.has_perm("pms.change_employeekeyresult")
|
|
|
|
|
or request.user.employee_get
|
|
|
|
|
in self.employee_objective_id.objective_id.managers.all()
|
|
|
|
|
or (
|
|
|
|
|
self.employee_objective_id.objective_id.self_employee_progress_update
|
|
|
|
|
and (
|
|
|
|
|
self.employee_objective_id.employee_id == request.user.employee_get
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
col = f"""
|
|
|
|
|
<input
|
|
|
|
|
id = "{self.id}"
|
|
|
|
|
type="number" class="oh-input p-1"
|
|
|
|
|
style="width: 100px;"
|
|
|
|
|
min="0"
|
|
|
|
|
value="{self.current_value}"
|
|
|
|
|
name="current_value"
|
|
|
|
|
onchange="delayedProgress(this)"
|
|
|
|
|
/>
|
|
|
|
|
"""
|
|
|
|
|
return col
|
|
|
|
|
return self.current_value
|
|
|
|
|
|
|
|
|
|
def get_progress_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For progress column
|
|
|
|
|
"""
|
|
|
|
|
col = f"""
|
|
|
|
|
<span class="progressPercentage"> {self.progress_percentage}%</span>
|
|
|
|
|
"""
|
|
|
|
|
return col
|
|
|
|
|
|
|
|
|
|
def status_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For status column
|
|
|
|
|
"""
|
|
|
|
|
update_url = reverse(
|
|
|
|
|
"employee-keyresult-update-status", kwargs={"kr_id": self.pk}
|
|
|
|
|
)
|
|
|
|
|
options = "".join(
|
|
|
|
|
f"<option value='{str(key)}' {'selected' if key == self.status else ''}>{str(value)}</option>"
|
|
|
|
|
for key, value in self.STATUS_CHOICES
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
col = f"""
|
|
|
|
|
<select
|
|
|
|
|
id="keyResultStatus" name="key_result_status"
|
|
|
|
|
hx-post="{update_url}"
|
|
|
|
|
hx-trigger="change" class="oh-table__editable-input w-100"
|
|
|
|
|
hx-on-htmx-after-request = "$('#reloadMessagesButton').click()"
|
|
|
|
|
hx-swap = "none"
|
|
|
|
|
>
|
|
|
|
|
{options}
|
|
|
|
|
</select>
|
|
|
|
|
"""
|
|
|
|
|
return col
|
|
|
|
|
|
|
|
|
|
def get_instance_id(self):
|
|
|
|
|
return self.pk
|
|
|
|
|
|
|
|
|
|
def current_value_col(self):
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/dashboard/current_value.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def progress_col(self):
|
|
|
|
|
|
|
|
|
|
return f'<div class="p-percentage">{self.progress_percentage}%</div>'
|
|
|
|
|
|
|
|
|
|
def target_value_col(self):
|
|
|
|
|
return f'<div data-value="{self.target_value}">{self.target_value}</div>'
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def update_kr_progress(self):
|
2023-11-28 19:50:41 +05:30
|
|
|
if self.target_value != 0:
|
2024-01-17 14:19:51 +05:30
|
|
|
self.progress_percentage = (
|
|
|
|
|
int(self.current_value) / int(self.target_value)
|
|
|
|
|
) * 100
|
|
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def clean(self):
|
|
|
|
|
from pms.forms import validate_date
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
super().clean()
|
|
|
|
|
start_date = self.start_date
|
|
|
|
|
end_date = self.end_date
|
|
|
|
|
# Check that start date is before end date
|
|
|
|
|
validate_date(start_date, end_date)
|
|
|
|
|
start_value = self.start_value
|
|
|
|
|
current_value = self.current_value
|
|
|
|
|
target_value = self.target_value
|
2024-10-19 16:06:08 +05:30
|
|
|
|
|
|
|
|
# Unique constraint employee_objective_id and key_result_id
|
2024-10-28 14:05:46 +05:30
|
|
|
if self.pk:
|
|
|
|
|
if (
|
|
|
|
|
EmployeeKeyResult.objects.filter(
|
|
|
|
|
key_result_id=self.key_result_id,
|
|
|
|
|
employee_objective_id=self.employee_objective_id,
|
|
|
|
|
)
|
|
|
|
|
.exclude(id=self.pk)
|
|
|
|
|
.exists()
|
|
|
|
|
):
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
_(
|
|
|
|
|
f"{self.employee_objective_id.employee_id} already assigned {self.key_result_id}."
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
if EmployeeKeyResult.objects.filter(
|
|
|
|
|
key_result_id=self.key_result_id,
|
|
|
|
|
employee_objective_id=self.employee_objective_id,
|
|
|
|
|
).exists():
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
_(
|
|
|
|
|
f"{self.employee_objective_id.employee_id} already assigned {self.key_result_id}."
|
|
|
|
|
)
|
2024-10-19 16:06:08 +05:30
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
if target_value == 0:
|
2024-03-10 19:37:46 +05:30
|
|
|
raise ValidationError(
|
|
|
|
|
{"target_value": _("The target value can't be zero.")}
|
|
|
|
|
)
|
2024-10-19 16:06:08 +05:30
|
|
|
if self.key_result_id.progress_type == "%" and target_value > 100:
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
{
|
|
|
|
|
"target_value": _(
|
|
|
|
|
"The key result progress type is in percentage, so the target value cannot exceed 100."
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
2024-03-08 22:35:28 +05:30
|
|
|
if start_value > current_value or start_value > target_value:
|
2024-03-10 19:37:46 +05:30
|
|
|
raise ValidationError(
|
|
|
|
|
"The start value can't be greater than current value or target value."
|
|
|
|
|
)
|
2025-06-11 14:36:06 +05:30
|
|
|
# if current_value > target_value:
|
|
|
|
|
# raise ValidationError(
|
|
|
|
|
# {
|
|
|
|
|
# "current_value": _(
|
|
|
|
|
# "The current value can't be greater than target value."
|
|
|
|
|
# )
|
|
|
|
|
# }
|
|
|
|
|
# )
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def save(self, *args, **kwargs):
|
2024-10-19 16:06:08 +05:30
|
|
|
if self.start_date and not self.end_date:
|
|
|
|
|
self.end_date = self.start_date + relativedelta(
|
|
|
|
|
days=self.key_result_id.duration
|
|
|
|
|
)
|
2024-03-26 14:11:47 +05:30
|
|
|
if not self.pk and not self.current_value:
|
2024-03-08 22:35:28 +05:30
|
|
|
self.current_value = self.start_value
|
2024-04-02 10:11:55 +05:30
|
|
|
if self.key_result_id:
|
|
|
|
|
self.key_result = self.key_result_id.title
|
2024-03-08 22:35:28 +05:30
|
|
|
self.update_kr_progress()
|
2023-10-19 12:39:13 +05:30
|
|
|
super().save(*args, **kwargs)
|
2024-03-08 22:35:28 +05:30
|
|
|
self.employee_objective_id.update_objective_progress()
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2024-07-17 14:09:12 +05:30
|
|
|
class meta:
|
|
|
|
|
"""
|
|
|
|
|
Meta class to add some additional options
|
|
|
|
|
"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-07-17 14:09:12 +05:30
|
|
|
unique_together = ("key_result_id", "employee_objective_id")
|
2024-03-10 19:37:46 +05:30
|
|
|
|
2023-10-26 12:54:15 +05:30
|
|
|
|
2024-04-24 12:16:28 +05:30
|
|
|
"""360degree feedback section"""
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class QuestionTemplate(HorillaModel):
|
2023-05-10 15:06:57 +05:30
|
|
|
"""question template creation"""
|
2023-09-09 14:10:09 +05:30
|
|
|
|
|
|
|
|
question_template = models.CharField(
|
2024-08-05 14:22:44 +05:30
|
|
|
max_length=100, null=False, blank=False, unique=True, verbose_name="Title"
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company"))
|
|
|
|
|
|
2024-12-06 15:57:10 +05:30
|
|
|
objects = HorillaCompanyManager("company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.question_template
|
|
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def question_count(self):
|
|
|
|
|
return self.question.count()
|
|
|
|
|
|
|
|
|
|
def action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/question_template/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_avatar(self):
|
|
|
|
|
"""
|
|
|
|
|
Method will retun the api to the avatar or path to the question template
|
|
|
|
|
"""
|
|
|
|
|
url = f"https://ui-avatars.com/api/?name={self.question_template}&background=random"
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_detail_url(self):
|
|
|
|
|
"""
|
|
|
|
|
Detail view url
|
|
|
|
|
"""
|
|
|
|
|
url = reverse_lazy(
|
|
|
|
|
"question-template-detailed-view", kwargs={"template_id": self.pk}
|
|
|
|
|
)
|
|
|
|
|
return url
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class Question(HorillaModel):
|
2023-05-10 15:06:57 +05:30
|
|
|
"""question creation"""
|
2023-09-09 14:10:09 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
QUESTION_TYPE_CHOICE = (
|
2023-09-09 14:10:09 +05:30
|
|
|
("1", _("Text")),
|
|
|
|
|
("2", _("Rating")),
|
|
|
|
|
("3", _("Boolean")),
|
|
|
|
|
("4", _("Multi-choices")),
|
|
|
|
|
("5", _("Likert")),
|
2023-05-10 15:06:57 +05:30
|
|
|
)
|
|
|
|
|
question = models.CharField(max_length=250, null=False, blank=False)
|
2023-09-09 14:10:09 +05:30
|
|
|
question_type = models.CharField(
|
|
|
|
|
choices=QUESTION_TYPE_CHOICE, max_length=100, null=True, blank=True
|
|
|
|
|
)
|
|
|
|
|
template_id = models.ForeignKey(
|
|
|
|
|
QuestionTemplate,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="question",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager("template_id__company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.question
|
|
|
|
|
|
|
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class QuestionOptions(HorillaModel):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""options for question"""
|
|
|
|
|
|
|
|
|
|
question_id = models.ForeignKey(
|
|
|
|
|
Question,
|
2023-09-19 15:56:53 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-09-09 14:10:09 +05:30
|
|
|
related_name="question_options",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
option_a = models.CharField(max_length=250, null=True, blank=True)
|
|
|
|
|
option_b = models.CharField(max_length=250, null=True, blank=True)
|
|
|
|
|
option_c = models.CharField(max_length=250, null=True, blank=True)
|
|
|
|
|
option_d = models.CharField(max_length=250, null=True, blank=True)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager("question_id__template_id__company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
|
2024-04-11 12:34:55 +05:30
|
|
|
class Feedback(HorillaModel):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""feedback model for creating feedback"""
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
STATUS_CHOICES = (
|
2023-09-09 14:10:09 +05:30
|
|
|
("On Track", _("On Track")),
|
|
|
|
|
("Behind", _("Behind")),
|
|
|
|
|
("Closed", _("Closed")),
|
|
|
|
|
("At Risk", _("At Risk")),
|
|
|
|
|
("Not Started", _("Not Started")),
|
2023-05-10 15:06:57 +05:30
|
|
|
)
|
2024-04-02 10:11:55 +05:30
|
|
|
PERIOD = (
|
|
|
|
|
("days", _("Days")),
|
|
|
|
|
("months", _("Months")),
|
|
|
|
|
("years", _("Years")),
|
|
|
|
|
)
|
2025-03-14 16:15:11 +05:30
|
|
|
review_cycle = models.CharField(
|
|
|
|
|
max_length=100, null=False, blank=False, verbose_name=_("Title")
|
|
|
|
|
)
|
2023-09-09 14:10:09 +05:30
|
|
|
manager_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
related_name="feedback_manager",
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
null=True,
|
2025-04-21 18:06:46 +05:30
|
|
|
blank=True,
|
2025-03-14 16:15:11 +05:30
|
|
|
verbose_name=_("Manager"),
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
|
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="feedback_employee",
|
|
|
|
|
null=False,
|
|
|
|
|
blank=False,
|
2025-03-14 16:15:11 +05:30
|
|
|
verbose_name=_("Employee"),
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
|
|
|
|
colleague_id = models.ManyToManyField(
|
2025-03-14 16:15:11 +05:30
|
|
|
Employee,
|
|
|
|
|
related_name="feedback_colleague",
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Colleague"),
|
2025-05-22 16:47:36 +05:30
|
|
|
help_text=_("Employees working on the same department."),
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
|
|
|
|
subordinate_id = models.ManyToManyField(
|
2025-03-14 16:15:11 +05:30
|
|
|
Employee,
|
|
|
|
|
related_name="feedback_subordinate",
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Subordinates"),
|
2025-05-22 16:47:36 +05:30
|
|
|
help_text=_(
|
|
|
|
|
"Employees for whom the feedback requester is the reporting manager"
|
|
|
|
|
),
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
2025-04-25 00:40:53 +05:30
|
|
|
others_id = models.ManyToManyField(
|
|
|
|
|
Employee,
|
|
|
|
|
related_name="feedback_others",
|
|
|
|
|
blank=True,
|
2025-05-22 16:47:36 +05:30
|
|
|
verbose_name=_("Other Employees"),
|
2025-04-25 00:40:53 +05:30
|
|
|
)
|
2023-09-09 14:10:09 +05:30
|
|
|
question_template_id = models.ForeignKey(
|
|
|
|
|
QuestionTemplate,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="feedback_question_template",
|
|
|
|
|
null=False,
|
|
|
|
|
blank=False,
|
2025-03-14 16:15:11 +05:30
|
|
|
verbose_name=_("Question Template"),
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
max_length=50, choices=STATUS_CHOICES, default="Not Started"
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
archive = models.BooleanField(null=True, blank=True, default=False)
|
2025-03-14 16:15:11 +05:30
|
|
|
start_date = models.DateField(null=False, blank=False, verbose_name=_("Start Date"))
|
|
|
|
|
end_date = models.DateField(null=True, blank=False, verbose_name=_("End Date"))
|
2023-09-09 14:10:09 +05:30
|
|
|
employee_key_results_id = models.ManyToManyField(
|
2025-03-14 16:15:11 +05:30
|
|
|
EmployeeKeyResult, blank=True, verbose_name=_("Key Result")
|
|
|
|
|
)
|
|
|
|
|
cyclic_feedback = models.BooleanField(
|
|
|
|
|
default=False, verbose_name=_("Is Cyclic Feedback")
|
|
|
|
|
)
|
|
|
|
|
cyclic_feedback_days_count = models.IntegerField(
|
|
|
|
|
blank=True, null=True, verbose_name=_("Cycle Period")
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
2024-04-02 10:11:55 +05:30
|
|
|
cyclic_feedback_period = models.CharField(
|
|
|
|
|
max_length=50, choices=PERIOD, blank=True, null=True
|
|
|
|
|
)
|
|
|
|
|
cyclic_next_start_date = models.DateField(null=True, blank=True)
|
|
|
|
|
cyclic_next_end_date = models.DateField(null=True, blank=True)
|
|
|
|
|
|
2024-01-17 14:19:51 +05:30
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
2024-04-24 12:16:28 +05:30
|
|
|
|
2024-04-02 10:11:55 +05:30
|
|
|
class Meta:
|
|
|
|
|
ordering = ["-id"]
|
2025-03-14 16:15:11 +05:30
|
|
|
verbose_name = _("Feedback")
|
|
|
|
|
verbose_name_plural = _("Feedbacks")
|
2024-04-02 10:11:55 +05:30
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
start_date = self.start_date
|
|
|
|
|
end_date = self.end_date
|
|
|
|
|
cyclic_feedback_period = self.cyclic_feedback_period
|
|
|
|
|
cyclic_feedback_days_count = self.cyclic_feedback_days_count
|
2024-04-24 12:16:28 +05:30
|
|
|
|
2024-04-02 10:11:55 +05:30
|
|
|
if cyclic_feedback_period == "months":
|
2024-04-24 12:16:28 +05:30
|
|
|
self.cyclic_next_start_date = self.start_date + relativedelta(
|
|
|
|
|
months=cyclic_feedback_days_count
|
|
|
|
|
)
|
|
|
|
|
self.cyclic_next_end_date = end_date + relativedelta(
|
|
|
|
|
months=cyclic_feedback_days_count
|
|
|
|
|
)
|
2024-04-02 10:11:55 +05:30
|
|
|
elif cyclic_feedback_period == "years":
|
2024-04-24 12:16:28 +05:30
|
|
|
self.cyclic_next_start_date = start_date + relativedelta(
|
|
|
|
|
years=cyclic_feedback_days_count
|
|
|
|
|
)
|
|
|
|
|
self.cyclic_next_end_date = end_date + relativedelta(
|
|
|
|
|
years=cyclic_feedback_days_count
|
|
|
|
|
)
|
2024-04-02 10:11:55 +05:30
|
|
|
elif cyclic_feedback_period == "days":
|
2024-04-24 12:16:28 +05:30
|
|
|
self.cyclic_next_start_date = start_date + relativedelta(
|
|
|
|
|
days=cyclic_feedback_days_count
|
|
|
|
|
)
|
|
|
|
|
self.cyclic_next_end_date = end_date + relativedelta(
|
|
|
|
|
days=cyclic_feedback_days_count
|
|
|
|
|
)
|
2024-04-02 10:11:55 +05:30
|
|
|
|
|
|
|
|
super().save(*args, **kwargs)
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-11-20 14:30:32 +05:30
|
|
|
return f"{self.employee_id.employee_first_name} - {self.review_cycle}"
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def due_days_diff(self):
|
|
|
|
|
"""
|
|
|
|
|
Returns number of days between current date and end_date.
|
|
|
|
|
"""
|
|
|
|
|
current_date = timezone.now().date()
|
|
|
|
|
if not self.end_date:
|
|
|
|
|
return None
|
|
|
|
|
return (self.end_date - current_date).days
|
|
|
|
|
|
|
|
|
|
def custom_status_style(self):
|
|
|
|
|
"""
|
|
|
|
|
method for rendering custom status col
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/360_feedback/custom_status_col.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def custom_actions_col(self):
|
|
|
|
|
"""
|
|
|
|
|
method for rendering custom actions col
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/360_feedback/custom_actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_individual_feedback(self):
|
|
|
|
|
"""
|
|
|
|
|
This method to get individual feedback
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse_lazy("feedback-detailed-view", kwargs={"id": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_feedback_due_date(self):
|
|
|
|
|
"""
|
|
|
|
|
Due display
|
|
|
|
|
"""
|
|
|
|
|
if self.status == "Closed":
|
|
|
|
|
return self.end_date.strftime("%b %d, %Y")
|
|
|
|
|
current_date = timezone.now().date()
|
|
|
|
|
date_diff = (self.end_date - current_date).days
|
|
|
|
|
|
|
|
|
|
status = (
|
|
|
|
|
"danger"
|
|
|
|
|
if self.end_date < current_date
|
|
|
|
|
else "warning" if self.end_date == current_date else "success"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
title_text = (
|
|
|
|
|
"Due today"
|
|
|
|
|
if self.end_date == current_date
|
|
|
|
|
else (
|
|
|
|
|
f"Over due by {abs(date_diff)} days"
|
|
|
|
|
if self.end_date < current_date
|
|
|
|
|
else f"Due in {date_diff} days"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
html = f"""
|
|
|
|
|
<span title="{title_text}">
|
|
|
|
|
<ion-icon
|
|
|
|
|
class="text-{status}"
|
|
|
|
|
name="time-sharp"
|
|
|
|
|
>
|
|
|
|
|
</ion-icon>
|
|
|
|
|
</span>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return f"{self.end_date.strftime('%b %d, %Y')} {html}"
|
|
|
|
|
|
|
|
|
|
def custom_due_in_col(self):
|
|
|
|
|
"""
|
|
|
|
|
This method fro custom due in col
|
|
|
|
|
"""
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/360_feedback/due_in_col.html",
|
|
|
|
|
context={"instance": self, "current_date": datetime.today()},
|
|
|
|
|
)
|
|
|
|
|
|
2024-09-19 10:26:41 +05:30
|
|
|
def requested_employees(self):
|
2025-04-25 00:40:53 +05:30
|
|
|
employees = set(self.subordinate_id.all())
|
|
|
|
|
employees.update(self.colleague_id.all())
|
|
|
|
|
employees.update(self.others_id.all())
|
|
|
|
|
if self.manager_id:
|
|
|
|
|
employees.add(self.manager_id)
|
|
|
|
|
if self.employee_id:
|
|
|
|
|
employees.add(self.employee_id)
|
|
|
|
|
return list(employees)
|
2024-09-19 10:26:41 +05:30
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def question_answer(self):
|
|
|
|
|
"""
|
|
|
|
|
Returns all the values list of question inside the template
|
|
|
|
|
"""
|
|
|
|
|
# Employee.objects.select_related()
|
|
|
|
|
return list(
|
|
|
|
|
self.feedback_answer.annotate(
|
|
|
|
|
answer_by=Concat(
|
|
|
|
|
"employee_id__employee_first_name",
|
|
|
|
|
Value(" "),
|
|
|
|
|
"employee_id__employee_last_name",
|
|
|
|
|
Value(" ("),
|
|
|
|
|
"employee_id__badge_id",
|
|
|
|
|
Value(")"),
|
|
|
|
|
),
|
|
|
|
|
).values(
|
|
|
|
|
"question_id__question",
|
|
|
|
|
"answer",
|
|
|
|
|
"answer_by",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
|
2024-01-17 14:19:51 +05:30
|
|
|
class AnonymousFeedback(models.Model):
|
|
|
|
|
"""feedback model for creating feedback"""
|
|
|
|
|
|
|
|
|
|
STATUS_CHOICES = (
|
|
|
|
|
("On Track", _("On Track")),
|
|
|
|
|
("Behind", _("Behind")),
|
|
|
|
|
("Closed", _("Closed")),
|
|
|
|
|
("At Risk", _("At Risk")),
|
|
|
|
|
("Not Started", _("Not Started")),
|
|
|
|
|
)
|
|
|
|
|
BASED_ON_CHOICES = (
|
|
|
|
|
("general", _("General")),
|
|
|
|
|
("employee", _("Employee")),
|
|
|
|
|
("department", _("Department")),
|
|
|
|
|
("job_position", _("Job Position")),
|
|
|
|
|
)
|
|
|
|
|
feedback_subject = models.CharField(max_length=100, null=False, blank=False)
|
|
|
|
|
based_on = models.CharField(
|
|
|
|
|
max_length=50, choices=BASED_ON_CHOICES, default="general"
|
|
|
|
|
)
|
|
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Employee"),
|
|
|
|
|
)
|
|
|
|
|
department_id = models.ForeignKey(
|
|
|
|
|
Department,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Department"),
|
|
|
|
|
)
|
|
|
|
|
job_position_id = models.ForeignKey(
|
|
|
|
|
JobPosition,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("Job Position"),
|
|
|
|
|
)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
max_length=50, choices=STATUS_CHOICES, default="Not Started"
|
|
|
|
|
)
|
|
|
|
|
created_at = models.DateField(auto_now_add=True)
|
|
|
|
|
archive = models.BooleanField(null=True, blank=True, default=False)
|
|
|
|
|
anonymous_feedback_id = models.CharField(
|
|
|
|
|
max_length=10, null=True, blank=False, editable=False
|
|
|
|
|
)
|
2024-03-10 19:37:46 +05:30
|
|
|
feedback_description = models.TextField(null=True, blank=True, max_length=255)
|
2024-04-11 12:34:55 +05:30
|
|
|
objects = models.Manager()
|
2024-01-17 14:19:51 +05:30
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"Feedback based on a {self.based_on}"
|
|
|
|
|
|
|
|
|
|
def clean(self, *args, **kwargs):
|
|
|
|
|
if self.based_on == "employee":
|
|
|
|
|
self._validate_required_field("employee_id", "Employee")
|
|
|
|
|
self.department_id = None
|
|
|
|
|
self.job_position_id = None
|
|
|
|
|
elif self.based_on == "department":
|
|
|
|
|
self._validate_required_field("department_id", "Department")
|
|
|
|
|
self.employee_id = None
|
|
|
|
|
self.job_position_id = None
|
|
|
|
|
elif self.based_on == "job_position":
|
|
|
|
|
self._validate_required_field("job_position_id", "Job Position")
|
|
|
|
|
self.employee_id = None
|
|
|
|
|
self.department_id = None
|
|
|
|
|
|
|
|
|
|
return super().clean(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def _validate_required_field(self, field_name, field_label):
|
|
|
|
|
if not getattr(self, field_name):
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
{
|
|
|
|
|
field_name: _(
|
|
|
|
|
f"The {field_label} field is required when the 'Based on' field is set to '{field_label}'."
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def anonymous_actions_col(self):
|
|
|
|
|
"""
|
|
|
|
|
method for rendering custom actions col
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/360_feedback/anonymous_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_based_on_value(self):
|
|
|
|
|
"""
|
|
|
|
|
return based on condition
|
|
|
|
|
"""
|
|
|
|
|
if self.based_on == "employee":
|
|
|
|
|
return f"Based On : {self.employee_id}"
|
|
|
|
|
elif self.based_on == "department":
|
|
|
|
|
return f"Based On : {self.department_id}"
|
|
|
|
|
elif self.based_on == "job_position":
|
|
|
|
|
return f"Based On : {self.job_position_id}"
|
|
|
|
|
else:
|
|
|
|
|
return "Based On : General"
|
|
|
|
|
|
|
|
|
|
def get_individual_anonymous_feedback(self):
|
|
|
|
|
"""
|
|
|
|
|
This method to get individual feedback
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
url = reverse_lazy("single-anonymous-feedback-view", kwargs={"obj_id": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
2024-01-17 14:19:51 +05:30
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
class Answer(models.Model):
|
2023-09-09 14:10:09 +05:30
|
|
|
"""feedback answer model"""
|
|
|
|
|
|
2023-05-10 15:06:57 +05:30
|
|
|
answer = models.JSONField(max_length=200, null=True, blank=True)
|
2023-09-09 14:10:09 +05:30
|
|
|
question_id = models.ForeignKey(
|
|
|
|
|
Question,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="answer_question_id",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="employee_answer",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
feedback_id = models.ForeignKey(
|
2023-09-19 15:56:53 +05:30
|
|
|
Feedback, on_delete=models.PROTECT, related_name="feedback_answer"
|
2023-09-09 14:10:09 +05:30
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
2023-05-10 15:06:57 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-11-20 14:30:32 +05:30
|
|
|
return f"{self.employee_id.employee_first_name} - {self.answer}"
|
2023-09-09 14:10:09 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class KeyResultFeedback(models.Model):
|
|
|
|
|
feedback_id = models.ForeignKey(
|
|
|
|
|
Feedback,
|
2023-09-19 15:56:53 +05:30
|
|
|
on_delete=models.PROTECT,
|
2023-09-09 14:10:09 +05:30
|
|
|
related_name="feedback_key_result",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee, on_delete=models.DO_NOTHING, related_name="employee_key_result"
|
|
|
|
|
)
|
2023-05-10 15:06:57 +05:30
|
|
|
answer = models.JSONField(max_length=200, null=True, blank=True)
|
2023-09-09 14:10:09 +05:30
|
|
|
key_result_id = models.ForeignKey(
|
|
|
|
|
EmployeeKeyResult,
|
|
|
|
|
related_name="key_result_feedback",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
)
|
2023-12-01 15:36:51 +05:30
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
2024-03-08 22:35:28 +05:30
|
|
|
|
|
|
|
|
|
2024-04-24 12:16:28 +05:30
|
|
|
class Meetings(HorillaModel):
|
|
|
|
|
title = models.CharField(max_length=100)
|
|
|
|
|
date = models.DateTimeField(null=True, blank=True)
|
2024-05-07 12:23:36 +05:30
|
|
|
employee_id = models.ManyToManyField(
|
2024-09-23 17:13:27 +05:30
|
|
|
Employee,
|
|
|
|
|
related_name="meeting_employee",
|
|
|
|
|
verbose_name=_("Employee"),
|
2024-05-07 12:23:36 +05:30
|
|
|
)
|
2024-04-24 12:16:28 +05:30
|
|
|
manager = models.ManyToManyField(Employee, related_name="meeting_manager")
|
2024-05-07 12:23:36 +05:30
|
|
|
answer_employees = models.ManyToManyField(
|
|
|
|
|
Employee,
|
|
|
|
|
blank=True,
|
|
|
|
|
related_name="meeting_answer_employees",
|
2024-09-23 17:13:27 +05:30
|
|
|
verbose_name=_("Answerable Employees"),
|
|
|
|
|
help_text=_(
|
|
|
|
|
"Select the employees who can respond to question template in this meeting's, if any are added."
|
|
|
|
|
),
|
2024-05-07 12:23:36 +05:30
|
|
|
)
|
2024-04-24 12:16:28 +05:30
|
|
|
question_template = models.ForeignKey(
|
|
|
|
|
QuestionTemplate, on_delete=models.PROTECT, null=True, blank=True
|
|
|
|
|
)
|
|
|
|
|
response = models.TextField(null=True, blank=True)
|
2024-09-23 17:13:27 +05:30
|
|
|
show_response = models.BooleanField(default=False, editable=False)
|
2025-08-01 17:58:33 +05:30
|
|
|
company_id = models.ForeignKey(
|
|
|
|
|
Company,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
editable=False,
|
|
|
|
|
verbose_name=_("Company"),
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
|
|
|
|
objects = HorillaCompanyManager()
|
2024-05-07 12:23:36 +05:30
|
|
|
|
2024-04-24 16:44:25 +05:30
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("Meetings")
|
2025-05-31 10:37:32 +05:30
|
|
|
verbose_name_plural = _("Meetings")
|
2024-04-24 12:16:28 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.title
|
|
|
|
|
|
2025-06-11 14:36:06 +05:30
|
|
|
def title_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For title column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/title.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def answerable_col(self):
|
|
|
|
|
"""
|
|
|
|
|
manager in detail view
|
|
|
|
|
"""
|
|
|
|
|
employees = self.answer_employees.all()
|
|
|
|
|
if employees:
|
|
|
|
|
employee_names_string = "<br>".join(
|
|
|
|
|
[str(employee) for employee in employees]
|
|
|
|
|
)
|
|
|
|
|
employee_title = _("Answerable employees")
|
|
|
|
|
return f'<span class="oh-timeoff-modal__stat-title">{employee_title}</span><span class="oh-timeoff-modal__stat-count">{employee_names_string}</span>'
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def detail_action(self):
|
|
|
|
|
"""
|
|
|
|
|
For answerable employees column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/detail_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def date_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For date column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/date.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def employees_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For employees column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/employees.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def managers_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For manager column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/managers.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def action_col(self):
|
|
|
|
|
"""
|
|
|
|
|
For action column
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="cbv/meetings/actions.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def employ_detail_col(self):
|
|
|
|
|
"""
|
|
|
|
|
employees in detail view
|
|
|
|
|
"""
|
|
|
|
|
employees = self.employee_id.all()
|
|
|
|
|
if employees:
|
|
|
|
|
employee_names_string = "<br>".join(
|
|
|
|
|
[str(employee) for employee in employees]
|
|
|
|
|
)
|
|
|
|
|
employee_title = _("Employees")
|
|
|
|
|
return f'<span class="oh-timeoff-modal__stat-title">{employee_title}</span><span class="oh-timeoff-modal__stat-count">{employee_names_string}</span>'
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def manager_detail_col(self):
|
|
|
|
|
"""
|
|
|
|
|
manager in detail view
|
|
|
|
|
"""
|
|
|
|
|
employees = self.manager.all()
|
|
|
|
|
if employees:
|
|
|
|
|
employee_names_string = "<br>".join(
|
|
|
|
|
[str(employee) for employee in employees]
|
|
|
|
|
)
|
|
|
|
|
employee_title = _("Managers")
|
|
|
|
|
return f'<span class="oh-timeoff-modal__stat-title">{employee_title}</span><span class="oh-timeoff-modal__stat-count">{employee_names_string}</span>'
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def mom_col(self):
|
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
|
|
|
if not self.response:
|
|
|
|
|
return "-"
|
|
|
|
|
if (
|
|
|
|
|
request.user.has_perm("pms.view_meetings")
|
|
|
|
|
or request.user.employee_get in self.manager.all()
|
|
|
|
|
):
|
|
|
|
|
return self.response
|
|
|
|
|
return "-" if not self.show_response else self.response
|
|
|
|
|
|
|
|
|
|
def diff_cell(self):
|
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
|
|
|
if not getattr(self, "request", None):
|
|
|
|
|
self.request = request
|
|
|
|
|
if request.user.employee_get in self.manager.all():
|
|
|
|
|
return f'style="background-color: rgba(255, 166, 0, 0.158);" '
|
|
|
|
|
|
|
|
|
|
def meeting_detail_view(self):
|
|
|
|
|
"""
|
|
|
|
|
detail view
|
|
|
|
|
"""
|
|
|
|
|
url = reverse("meetings-detail-view", kwargs={"pk": self.pk})
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_avatar(self):
|
|
|
|
|
"""
|
|
|
|
|
Method will retun the api to the avatar or path to the profile image
|
|
|
|
|
"""
|
|
|
|
|
url = f"https://ui-avatars.com/api/?name={self.title}&background=random"
|
|
|
|
|
return url
|
|
|
|
|
|
2025-08-01 17:58:33 +05:30
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
|
|
|
selected_company = request.session.get("selected_company")
|
|
|
|
|
if (
|
|
|
|
|
not self.id
|
|
|
|
|
and not self.company_id
|
|
|
|
|
and selected_company
|
|
|
|
|
and selected_company != "all"
|
|
|
|
|
):
|
|
|
|
|
self.company_id = Company.find(selected_company)
|
|
|
|
|
|
|
|
|
|
super().save()
|
|
|
|
|
|
2024-04-24 12:16:28 +05:30
|
|
|
|
|
|
|
|
class MeetingsAnswer(models.Model):
|
|
|
|
|
"""feedback answer model"""
|
|
|
|
|
|
|
|
|
|
answer = models.JSONField(max_length=200, null=True, blank=True)
|
|
|
|
|
question_id = models.ForeignKey(
|
|
|
|
|
Question,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="meeting_answer_question_id",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="employee_meeting_answer",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
2024-05-07 12:23:36 +05:30
|
|
|
verbose_name="Employee",
|
2024-04-24 12:16:28 +05:30
|
|
|
)
|
|
|
|
|
meeting_id = models.ForeignKey(
|
|
|
|
|
Meetings, on_delete=models.PROTECT, related_name="meeting_answer"
|
|
|
|
|
)
|
|
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.employee_id.employee_first_name} - {self.answer}"
|
|
|
|
|
|
|
|
|
|
|
2024-09-12 14:11:50 +05:30
|
|
|
class EmployeeBonusPoint(HorillaModel):
|
2024-08-27 17:34:55 +05:30
|
|
|
employee_id = models.ForeignKey(
|
|
|
|
|
Employee,
|
|
|
|
|
on_delete=models.DO_NOTHING,
|
|
|
|
|
related_name="employe_bonus_point",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name="Employee",
|
|
|
|
|
)
|
|
|
|
|
bonus_point = models.IntegerField(default=0)
|
2024-08-30 14:51:21 +05:30
|
|
|
instance = models.CharField(max_length=150, null=True, blank=True)
|
2024-08-27 17:34:55 +05:30
|
|
|
based_on = models.CharField(max_length=150)
|
2024-09-12 14:11:50 +05:30
|
|
|
bonus_point_id = models.ForeignKey(
|
|
|
|
|
BonusPoint,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="employeebonuspoint_set",
|
|
|
|
|
)
|
2024-12-06 15:57:10 +05:30
|
|
|
objects = HorillaCompanyManager("employee_id__employee_work_info__company_id")
|
2024-08-27 17:34:55 +05:30
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.employee_id.employee_first_name} - {self.bonus_point}"
|
|
|
|
|
|
2024-08-30 14:51:21 +05:30
|
|
|
def action_template(self):
|
|
|
|
|
"""
|
|
|
|
|
This method for get custom column for managers.
|
|
|
|
|
"""
|
|
|
|
|
return render_template(
|
|
|
|
|
path="bonus/bonus_point_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
2024-09-12 14:11:50 +05:30
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
if not BonusPoint.objects.filter(employee_id=self.employee_id).exists():
|
|
|
|
|
bonus_point = BonusPoint.objects.create(
|
|
|
|
|
employee_id=self.employee_id,
|
|
|
|
|
points=self.bonus_point,
|
|
|
|
|
reason=self.based_on,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
bonus_point = BonusPoint.objects.get(employee_id=self.employee_id)
|
|
|
|
|
bonus_point.points += self.bonus_point
|
|
|
|
|
bonus_point.reason = self.based_on
|
|
|
|
|
bonus_point.save()
|
|
|
|
|
|
2024-08-27 17:34:55 +05:30
|
|
|
|
|
|
|
|
class BonusPointSetting(models.Model):
|
|
|
|
|
MODEL_CHOICES = [
|
2024-08-30 14:51:21 +05:30
|
|
|
("pms.models.EmployeeObjective", _("Objective")),
|
|
|
|
|
("pms.models.EmployeeKeyResult", _("Key Result")),
|
2024-08-27 17:34:55 +05:30
|
|
|
]
|
|
|
|
|
if apps.is_installed("project"):
|
|
|
|
|
MODEL_CHOICES += [
|
2024-08-30 14:51:21 +05:30
|
|
|
("project.models.Task", _("Task")),
|
|
|
|
|
("project.models.Project", _("Project")),
|
2024-08-27 17:34:55 +05:30
|
|
|
]
|
|
|
|
|
BONUS_FOR = [
|
2024-08-30 14:51:21 +05:30
|
|
|
("completed", _("Completing")),
|
|
|
|
|
("Closed", _("Closing")),
|
2024-08-27 17:34:55 +05:30
|
|
|
]
|
|
|
|
|
CONDITIONS = [
|
|
|
|
|
("=", "="),
|
|
|
|
|
(">", ">"),
|
|
|
|
|
("<", "<"),
|
|
|
|
|
("<=", "<="),
|
|
|
|
|
(">=", ">="),
|
|
|
|
|
]
|
|
|
|
|
FIELD_1 = [
|
2024-08-30 14:51:21 +05:30
|
|
|
("complition_date", _("Completion Date")),
|
2024-08-27 17:34:55 +05:30
|
|
|
]
|
|
|
|
|
FIELD_2 = [
|
2024-08-30 14:51:21 +05:30
|
|
|
("end_date", _("End Date")),
|
|
|
|
|
]
|
|
|
|
|
APPLECABLE_FOR = [
|
|
|
|
|
("owner", _("Owner")),
|
|
|
|
|
("members", _("Members")),
|
|
|
|
|
("managers", _("Managers")),
|
2024-08-27 17:34:55 +05:30
|
|
|
]
|
|
|
|
|
model = models.CharField(max_length=100, choices=MODEL_CHOICES, null=False)
|
2024-08-30 14:51:21 +05:30
|
|
|
applicable_for = models.CharField(
|
|
|
|
|
max_length=50, choices=APPLECABLE_FOR, null=True, blank=True
|
|
|
|
|
)
|
2024-08-27 17:34:55 +05:30
|
|
|
bonus_for = models.CharField(max_length=25, choices=BONUS_FOR)
|
|
|
|
|
field_1 = models.CharField(max_length=25, choices=FIELD_1, null=True, blank=True)
|
|
|
|
|
conditions = models.CharField(
|
|
|
|
|
max_length=25, choices=CONDITIONS, null=True, blank=True
|
|
|
|
|
)
|
|
|
|
|
field_2 = models.CharField(max_length=25, choices=FIELD_2, null=True, blank=True)
|
|
|
|
|
points = models.IntegerField(default=0, validators=[MinValueValidator(0)])
|
|
|
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
|
|
|
|
|
|
def get_model_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Display model
|
|
|
|
|
"""
|
|
|
|
|
return dict(BonusPointSetting.MODEL_CHOICES).get(self.model)
|
|
|
|
|
|
|
|
|
|
def get_bonus_for_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Display bonus_for
|
|
|
|
|
"""
|
|
|
|
|
return dict(BonusPointSetting.BONUS_FOR).get(self.bonus_for)
|
|
|
|
|
|
|
|
|
|
def get_field_1_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Display field_1
|
|
|
|
|
"""
|
|
|
|
|
return dict(BonusPointSetting.FIELD_1).get(self.field_1)
|
|
|
|
|
|
|
|
|
|
def get_field_2_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Display field_2
|
|
|
|
|
"""
|
|
|
|
|
return dict(BonusPointSetting.FIELD_2).get(self.field_2)
|
|
|
|
|
|
2024-08-30 14:51:21 +05:30
|
|
|
def get_applicable_for_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Display applicable_for
|
|
|
|
|
"""
|
|
|
|
|
return dict(BonusPointSetting.APPLECABLE_FOR).get(self.applicable_for)
|
|
|
|
|
|
2024-08-27 17:34:55 +05:30
|
|
|
def get_condition(self):
|
|
|
|
|
"""
|
|
|
|
|
Get the condition for bonus
|
|
|
|
|
"""
|
|
|
|
|
return f" {dict(BonusPointSetting.FIELD_1).get(self.field_1)} {self.conditions} {dict(BonusPointSetting.FIELD_2).get(self.field_2)}"
|
|
|
|
|
|
|
|
|
|
def action_template(self):
|
|
|
|
|
"""
|
|
|
|
|
This method for get custom column for managers.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
path="bonus/bonus_seetting_action.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
2024-08-30 14:51:21 +05:30
|
|
|
def is_active_toggle(self):
|
|
|
|
|
"""
|
|
|
|
|
For toggle is_active field
|
|
|
|
|
"""
|
|
|
|
|
return render_template(
|
|
|
|
|
path="bonus/is_active_toggle.html",
|
|
|
|
|
context={"instance": self},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def create_employee_bonus(self, employee, field_1, field_2, instance):
|
2024-08-27 17:34:55 +05:30
|
|
|
"""
|
|
|
|
|
For creating employee bonus
|
|
|
|
|
"""
|
|
|
|
|
operator_mapping = {
|
|
|
|
|
"=": operator.eq,
|
|
|
|
|
"!=": operator.ne,
|
|
|
|
|
"<": operator.lt,
|
|
|
|
|
">": operator.gt,
|
|
|
|
|
"<=": operator.le,
|
|
|
|
|
">=": operator.ge,
|
|
|
|
|
}
|
2024-08-30 14:51:21 +05:30
|
|
|
if (
|
|
|
|
|
operator_mapping[self.conditions](field_1, field_2)
|
|
|
|
|
) and not EmployeeBonusPoint.objects.filter(
|
|
|
|
|
employee_id=employee,
|
|
|
|
|
instance=instance,
|
|
|
|
|
based_on=(f"{self.get_bonus_for_display()} {instance}"),
|
|
|
|
|
).exists():
|
2024-08-27 17:34:55 +05:30
|
|
|
EmployeeBonusPoint(
|
|
|
|
|
employee_id=employee,
|
2024-08-30 14:51:21 +05:30
|
|
|
based_on=(f"{self.get_bonus_for_display()} {instance}"),
|
2024-08-27 17:34:55 +05:30
|
|
|
bonus_point=self.points,
|
2024-08-30 14:51:21 +05:30
|
|
|
instance=instance,
|
2024-08-27 17:34:55 +05:30
|
|
|
).save()
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
2024-09-02 00:13:52 +05:30
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"Bonus point {self.get_model_display()}"
|
|
|
|
|
|
2024-08-27 17:34:55 +05:30
|
|
|
|
2024-03-08 22:35:28 +05:30
|
|
|
def manipulate_existing_data():
|
2024-03-10 19:37:46 +05:30
|
|
|
from dateutil.relativedelta import relativedelta
|
2024-05-07 12:23:36 +05:30
|
|
|
|
2024-03-10 19:37:46 +05:30
|
|
|
try:
|
2024-03-28 14:28:18 +05:30
|
|
|
for emp_objective in EmployeeObjective.objects.exclude(objective=None):
|
|
|
|
|
objective, _ = Objective.objects.get_or_create(
|
|
|
|
|
title=emp_objective.objective
|
|
|
|
|
)
|
|
|
|
|
objective.duration = 20
|
|
|
|
|
objective.save()
|
|
|
|
|
emp_objective.end_date = emp_objective.start_date + relativedelta(days=20)
|
|
|
|
|
emp_objective.objective_id = objective
|
|
|
|
|
emp_objective.objective = None
|
|
|
|
|
emp_objective.objective_description = None
|
|
|
|
|
emp_objective.save()
|
|
|
|
|
|
|
|
|
|
for e_kr in EmployeeKeyResult.objects.exclude(key_result=None):
|
|
|
|
|
kr, _ = KeyResult.objects.get_or_create(title=e_kr.key_result)
|
|
|
|
|
kr.duration = 2
|
|
|
|
|
kr.save()
|
|
|
|
|
e_kr.end_date = e_kr.start_date + relativedelta(days=2)
|
|
|
|
|
e_kr.key_result = None
|
|
|
|
|
e_kr.key_result_description = None
|
|
|
|
|
e_kr.key_result_id = kr
|
|
|
|
|
e_kr.save()
|
2024-03-10 19:37:46 +05:30
|
|
|
|
|
|
|
|
except Exception as e:
|
2024-03-28 14:28:18 +05:30
|
|
|
return
|
2024-03-08 22:35:28 +05:30
|
|
|
|
|
|
|
|
|
2024-03-28 14:28:18 +05:30
|
|
|
manipulate_existing_data()
|