[ADD] PMS: Employee bonus point feature

This commit is contained in:
Horilla
2024-08-27 17:34:55 +05:30
parent 2078776d36
commit 9720826b9b
17 changed files with 1132 additions and 0 deletions

View File

@@ -10,7 +10,9 @@ from simple_history.admin import SimpleHistoryAdmin
from .models import (
AnonymousFeedback,
Answer,
BonusPointSetting,
Comment,
EmployeeBonusPoint,
EmployeeKeyResult,
EmployeeObjective,
Feedback,
@@ -37,3 +39,5 @@ admin.site.register(Objective)
admin.site.register(KeyResultFeedback)
admin.site.register(Meetings)
admin.site.register(Comment, SimpleHistoryAdmin)
admin.site.register(BonusPointSetting)
admin.site.register(EmployeeBonusPoint)

View File

@@ -23,3 +23,11 @@ class PmsConfig(AppConfig):
path("pms/", include("pms.urls")),
)
super().ready()
try:
from pms.signals import start_automation
start_automation()
except:
"""
Migrations are not affected yet
"""

245
pms/cbvs.py Normal file
View File

@@ -0,0 +1,245 @@
from typing import Any
from django.http import HttpResponse
from django.shortcuts import render
from horilla_views.generic.cbv import views
from pms import models
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _trans
from django.contrib import messages
from pms.filters import BonusPointSettingFilter,EmployeeBonusPointFilter
from pms.forms import BonusPointSettingForm,EmployeeBonusPointForm
#================Models for BonusPointSetting==============
class BonusPointSettingSectionView(views.HorillaSectionView):
"""
BonusPointSetting SectionView
"""
nav_url = reverse_lazy("bonus-point-setting-nav")
view_url = reverse_lazy("bonus-point-setting-list-view")
view_container_id = "listContainer"
# script_static_paths = [
# "static/automation/automation.js",
# ]
template_name = "bonus/bonus_point_setting_section.html"
class BonusPointSettingNavView(views.HorillaNavView):
"""
BonusPointSetting nav view
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.create_attrs = f"""
hx-get="{reverse_lazy("create-bonus-point-setting")}"
hx-target="#genericModalBody"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
"""
nav_title = _trans("Bonus Point Setting")
search_url = reverse_lazy("bonus-point-setting-list-view")
search_swap_target = "#listContainer"
class BonusPointSettingFormView(views.HorillaFormView):
"""
BonusPointSettingForm View
"""
form_class = BonusPointSettingForm
model = models.BonusPointSetting
new_display_title = _trans("Create Bonus Point Setting")
# template_name = "bonus/bonus_form.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
instance = models.BonusPointSetting.objects.filter(pk=self.kwargs["pk"]).first()
kwargs["instance"] = instance
return kwargs
def get_context_data(self, **kwargs):
context= super().get_context_data(**kwargs)
return context
def form_invalid(self, form: Any) -> HttpResponse:
if not form.is_valid():
errors = form.errors.as_data()
return render(
self.request, self.template_name, {"form": form, "errors": errors}
)
return super().form_invalid(form)
def form_valid(self, form: BonusPointSettingForm) -> views.HttpResponse:
if form.is_valid():
message = "Bonus Point Setting added"
if form.instance.pk:
message = "Bonus Point Setting updated"
form.save()
messages.success(self.request, _trans(message))
return self.HttpResponse()
return super().form_valid(form)
class BonusPointSettingListView(views.HorillaListView):
"""
BnusPointSetting list view
"""
model = models.BonusPointSetting
search_url = reverse_lazy("bonus-point-setting-list-view")
filter_class = BonusPointSettingFilter
action_method = "action_template"
# actions = [
# {
# "action": "Edit",
# "icon": "create-outline",
# "attrs": """
# class="oh-btn oh-btn--light-bkg w-100"
# hx-get="{edit_url}?instance_ids={ordered_ids}"
# hx-target="#genericModalBody"
# data-target="#genericModal"
# data-toggle="oh-modal-toggle"
# """,
# },
# {
# "action": "Delete",
# "icon": "trash-outline",
# "attrs": """
# class="oh-btn oh-btn--light-bkg w-100 tex-danger"
# onclick="
# event.stopPropagation();
# confirm('Do you want to delete the bonus point setting?','{delete_url}')
# "
# """,
# },
# ]
columns = [
("Model", "get_model_display"),
("Bonus For", "get_bonus_for_display"),
("Condition", "get_condition"),
("Points", 'points'),
("Is Active",'is_active'),
]
#================Models for EmployeeBonusPoint==============
class EmployeeBonusPointSectionView(views.HorillaSectionView):
"""
EmployeeBonusPoint SectionView
"""
nav_url = reverse_lazy("employee-bonus-point-nav")
view_url = reverse_lazy("employee-bonus-point-list-view")
view_container_id = "listContainer"
# script_static_paths = [
# "static/automation/automation.js",
# ]
template_name = "bonus/employee_bonus_point_section.html"
class EmployeeBonusPointNavView(views.HorillaNavView):
"""
BonusPoint nav view
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.create_attrs = f"""
hx-get="{reverse_lazy("create-employee-bonus-point")}"
hx-target="#genericModalBody"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
"""
nav_title = _trans("Employee Bonus Point ")
search_url = reverse_lazy("employee-bonus-point-list-view")
search_swap_target = "#listContainer"
class EmployeeBonusPointFormView(views.HorillaFormView):
"""
BonusPointForm View
"""
form_class = EmployeeBonusPointForm
model = models.EmployeeBonusPoint
new_display_title = _trans("Create Employee Bonus Point ")
# template_name = "bonus/bonus_form.html"
def get_context_data(self, **kwargs):
context= super().get_context_data(**kwargs)
return context
def form_invalid(self, form: Any) -> HttpResponse:
if not form.is_valid():
errors = form.errors.as_data()
return render(
self.request, self.template_name, {"form": form, "errors": errors}
)
return super().form_invalid(form)
def form_valid(self, form: EmployeeBonusPointForm) -> views.HttpResponse:
if form.is_valid():
message = "Bonus Point added"
if form.instance.pk:
message = "Bonus Point updated"
form.save()
messages.success(self.request, _trans(message))
return self.HttpResponse()
return super().form_valid(form)
class EmployeeBonusPointListView(views.HorillaListView):
"""
BnusPoint list view
"""
model = models.EmployeeBonusPoint
search_url = reverse_lazy("employee-bonus-point-list-view")
filter_class = EmployeeBonusPointFilter
# actions = [
# {
# "action": "Edit",
# "icon": "create-outline",
# "attrs": """
# class="oh-btn oh-btn--light-bkg w-100"
# hx-get="{edit_url}?instance_ids={ordered_ids}"
# hx-target="#genericModalBody"
# data-target="#genericModal"
# data-toggle="oh-modal-toggle"
# """,
# },
# {
# "action": "Delete",
# "icon": "trash-outline",
# "attrs": """
# class="oh-btn oh-btn--light-bkg w-100 tex-danger"
# onclick="
# event.stopPropagation();
# confirm('Do you want to delete the automation?','{delete_url}')
# "
# """,
# },
# ]
columns = [
("Employee", "employee_id"),
("Bonus Point", "bonus_point"),
("Based On",'based_on'),
]

View File

@@ -15,6 +15,8 @@ from django_filters import DateFilter
from base.filters import FilterSet
from base.methods import reload_queryset
from pms.models import (
BonusPointSetting,
EmployeeBonusPoint,
EmployeeKeyResult,
EmployeeObjective,
Feedback,
@@ -407,3 +409,75 @@ class MeetingsFilter(FilterSet):
# q_objects |= Q(**{key: value})
# return queryset.filter(q_objects)
# return super().filter_queryset(queryset)
class BonusPointSettingFilter(FilterSet):
"""
Filter through BonusPointSetting model
"""
# search = django_filters.CharFilter(method="search_method")
# start_date_from = django_filters.DateFilter(
# field_name="start_date",
# lookup_expr="gte",
# widget=forms.DateInput(attrs={"type": "date"}),
# )
# start_date_till = django_filters.DateFilter(
# field_name="start_date",
# lookup_expr="lte",
# widget=forms.DateInput(attrs={"type": "date"}),
# )
# end_date_from = django_filters.DateFilter(
# field_name="end_date",
# lookup_expr="gte",
# widget=forms.DateInput(attrs={"type": "date"}),
# )
# end_date_till = django_filters.DateFilter(
# field_name="end_date",
# lookup_expr="lte",
# widget=forms.DateInput(attrs={"type": "date"}),
# )
class Meta:
model = BonusPointSetting
fields = "__all__"
def search_method(self, queryset, _, value: str):
"""
This method is used to search employees and objective
"""
values = value.split(" ")
empty = queryset.model.objects.none()
for split in values:
empty = (
empty
| (queryset.filter(employee_id__employee_first_name__icontains=split))
| (queryset.filter(employee_id__employee_last_name__icontains=split))
)
return empty.distinct()
class EmployeeBonusPointFilter(FilterSet):
"""
Filter through BonusPointSetting model
"""
class Meta:
model = EmployeeBonusPoint
fields = "__all__"
def search_method(self, queryset, _, value: str):
"""
This method is used to search employees and objective
"""
values = value.split(" ")
empty = queryset.model.objects.none()
for split in values:
empty = (
empty
| (queryset.filter(employee_id__employee_first_name__icontains=split))
| (queryset.filter(employee_id__employee_last_name__icontains=split))
)
return empty.distinct()

View File

@@ -18,6 +18,7 @@ from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from base.forms import ModelForm as BaseForm
from base.forms import ModelForm as MF
from base.methods import reload_queryset
from employee.filters import EmployeeFilter
from employee.models import Department, JobPosition
@@ -25,8 +26,10 @@ from horilla_widgets.widgets.horilla_multi_select_field import HorillaMultiSelec
from horilla_widgets.widgets.select_widgets import HorillaMultiSelectWidget
from pms.models import (
AnonymousFeedback,
BonusPointSetting,
Comment,
Employee,
EmployeeBonusPoint,
EmployeeKeyResult,
EmployeeObjective,
Feedback,
@@ -1069,3 +1072,78 @@ class MeetingsForm(BaseForm):
self.fields["answer_employees"].queryset = employees
except:
pass
class BonusPointSettingForm(MF):
"""
BonusPointSetting form
"""
# condition_html = forms.CharField(widget=forms.HiddenInput())
# condition_querystring = forms.CharField(widget=forms.HiddenInput())
# cols = {"template_attachments": 12}
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# if not self.data:
# mail_to = []
# initial = []
# mail_details_choice = []
# if self.instance.pk:
# mail_to = generate_choices(self.instance.model)[0]
# mail_details_choice = generate_choices(self.instance.model)[1]
# self.fields["mail_to"] = forms.MultipleChoiceField(choices=mail_to)
# self.fields["mail_details"] = forms.ChoiceField(
# choices=mail_details_choice,
# help_text="Fill mail template details(reciever/instance, `self` will be the person who trigger the automation)",
# )
# self.fields["mail_to"].initial = initial
# attrs = self.fields["mail_to"].widget.attrs
# attrs["class"] = "oh-select oh-select-2 w-100"
# attrs = self.fields["model"].widget.attrs
# attrs["onchange"] = "getToMail($(this))"
# self.fields["mail_template"].empty_label = None
# attrs = attrs.copy()
# del attrs["onchange"]
# self.fields["mail_details"].widget.attrs = attrs
# if self.instance.pk:
# self.fields["condition"].initial = self.instance.condition_html
# self.fields["condition_html"].initial = self.instance.condition_html
# self.fields["condition_querystring"].initial = (
# self.instance.condition_querystring
# )
class Meta:
model = BonusPointSetting
fields = "__all__"
# def as_p(self):
# """
# Render the form fields as HTML table rows with Bootstrap styling.
# """
# context = {"form": self}
# table_html = render_to_string("horilla_form.html", context)
# return table_html
# def save(self, commit: bool = ...) -> Any:
# self.instance: MailAutomation = self.instance
# condition_querystring = self.cleaned_data["condition_querystring"]
# condition_html = self.cleaned_data["condition_html"]
# mail_to = self.data.getlist("mail_to")
# self.instance.mail_to = str(mail_to)
# self.instance.mail_details = self.data["mail_details"]
# self.instance.condition_querystring = condition_querystring
# self.instance.condition_html = condition_html
# return super().save(commit)
class EmployeeBonusPointForm(MF):
"""
EmployeeBonusPoint form
"""
class Meta:
model = EmployeeBonusPoint
fields = "__all__"

View File

@@ -1,8 +1,15 @@
import operator
from datetime import date
from typing import Iterable
from dateutil.relativedelta import relativedelta
from django import forms
from django.apps import apps
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models.signals import post_delete, post_save, pre_save
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from base.horilla_company_manager import HorillaCompanyManager
@@ -11,6 +18,8 @@ from employee.models import Employee
from horilla.models import HorillaModel
from horilla_audit.methods import get_diff
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
from horilla_automations.methods.methods import get_model_class
from horilla_views.cbv_methods import render_template
"""Objectives and key result section"""
@@ -723,6 +732,160 @@ class MeetingsAnswer(models.Model):
return f"{self.employee_id.employee_first_name} - {self.answer}"
class EmployeeBonusPoint(models.Model):
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)
based_on = models.CharField(max_length=150)
def __str__(self):
return f"{self.employee_id.employee_first_name} - {self.bonus_point}"
class BonusPointSetting(models.Model):
MODEL_CHOICES = [
("pms.models.EmployeeObjective", "Objective"),
("pms.models.EmployeeKeyResult", "Key Result"),
]
if apps.is_installed("project"):
MODEL_CHOICES += [
("project.models.Task", "Task"),
("project.models.Project", "Project"),
]
BONUS_FOR = [
("completed", "Completing"),
]
CONDITIONS = [
("=", "="),
(">", ">"),
("<", "<"),
("<=", "<="),
(">=", ">="),
]
FIELD_1 = [
("complition_date", "Completion Date"),
]
FIELD_2 = [
("end_date", "End Date"),
]
model = models.CharField(max_length=100, choices=MODEL_CHOICES, null=False)
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)
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},
)
def create_employee_bonus(self, employee, field_1, field_2):
"""
For creating employee bonus
"""
operator_mapping = {
"=": operator.eq,
"!=": operator.ne,
"<": operator.lt,
">": operator.gt,
"<=": operator.le,
">=": operator.ge,
}
if operator_mapping[self.conditions](field_1, field_2):
EmployeeBonusPoint(
employee_id=employee,
based_on=(f"{self.get_bonus_for_display} {self.model}"),
bonus_point=self.points,
).save()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
model_class = get_model_class(self.model)
def create_signal_handler(name, bonus_point_setting):
def signal_handler(sender, instance, created, **kwargs):
"""
Signal handler for post-save events of the model instances.
"""
# request = getattr(_thread_locals, "request", None)
# previous_record = getattr(_thread_locals, "previous_record", None)
# previous_instance = None
# if previous_record:
# previous_instance = previous_record["instance"]
# if BonusPointSetting.objects.filter(model='Task').exists():
# bonus_point_settings = BonusPointSetting.objects.filter(model='Task')
# for bs in bonus_point_settings:
field_1 = date.today()
field_2 = instance.end_date
if bonus_point_setting.bonus_for == instance.status:
for employee in instance.task_members.all():
bonus_point_setting.create_employee_bonus(
employee, field_1, field_2
)
signal_handler.__name__ = name
signal_handler.model_class = model_class
signal_handler.bonus_point_setting = bonus_point_setting
return signal_handler
# Create and connect the signal handler
handler_name = f"{self.id}_signal_handler"
dynamic_signal_handler = create_signal_handler(handler_name, self)
# SIGNAL_HANDLERS.append(dynamic_signal_handler)
post_save.connect(
dynamic_signal_handler, sender=dynamic_signal_handler.model_class
)
def manipulate_existing_data():
from dateutil.relativedelta import relativedelta

View File

@@ -33,6 +33,10 @@ SUBMENUS = [
"redirect": reverse("view-key-result"),
"accessibility": "pms.sidebar.key_result_accessibility",
},
{
"menu": trans("Employee Bonus Point"),
"redirect": reverse("employee-bonus-point"),
},
{
"menu": trans("Period"),
"redirect": reverse("period-view"),

233
pms/signals.py Normal file
View File

@@ -0,0 +1,233 @@
"""
pms/signals.py
"""
import copy
import logging
import threading
import types
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from horilla.horilla_middlewares import _thread_locals
from pms.models import BonusPointSetting
from datetime import date
SIGNAL_HANDLERS = []
INSTANCE_HANDLERS = []
def start_automation():
"""
Automation signals
"""
from horilla_automations.methods.methods import get_model_class, split_query_string
@receiver(post_delete, sender=BonusPointSetting)
@receiver(post_save, sender=BonusPointSetting)
def automation_pre_create(sender, instance, **kwargs):
"""
signal method to handle automation post save
"""
start_connection()
track_previous_instance()
def clear_connection():
"""
Method to clear signals handlers
"""
for handler in SIGNAL_HANDLERS:
post_save.disconnect(handler, sender=handler.model_class)
SIGNAL_HANDLERS.clear()
def create_post_bulk_update_handler(automation, model_class, query_strings):
def post_bulk_update_handler(sender, queryset, *args, **kwargs):
def _bulk_update_thread_handler(
queryset, previous_queryset_copy, automation
):
request = getattr(queryset, "request", None)
if request:
for index, instance in enumerate(queryset):
previous_instance = previous_queryset_copy[index]
send_automated_mail(
request,
False,
automation,
query_strings,
instance,
previous_instance,
)
previous_bulk_record = getattr(_thread_locals, "previous_bulk_record", None)
previous_queryset = None
if previous_bulk_record:
previous_queryset = previous_bulk_record["queryset"]
previous_queryset_copy = previous_bulk_record["queryset_copy"]
bulk_thread = threading.Thread(
target=_bulk_update_thread_handler,
args=(queryset, previous_queryset_copy, automation),
)
bulk_thread.start()
func_name = f"{automation.method_title}_post_bulk_signal_handler"
# Dynamically create a function with a unique name
handler = types.FunctionType(
post_bulk_update_handler.__code__,
globals(),
name=func_name,
argdefs=post_bulk_update_handler.__defaults__,
closure=post_bulk_update_handler.__closure__,
)
# Set additional attributes on the function
handler.model_class = model_class
handler.automation = automation
return handler
def start_connection():
"""
Method to start signal connection accordingly to the automation
"""
clear_connection()
bonus_point_settings = BonusPointSetting.objects.filter(is_active=True)
for bonus_point_setting in bonus_point_settings:
# condition_querystring = bonus_point_setting.condition_querystring.replace(
# "automation_multiple_", ""
# )
# query_strings = split_query_string(condition_querystring)
# model_path should me in the form of pms.models.Objective
model_path = bonus_point_setting.model
model_class = get_model_class(model_path)
# handler = create_post_bulk_update_handler(
# bonus_point_setting, model_class, query_strings
# )
# SIGNAL_HANDLERS.append(handler)
# post_bulk_update.connect(handler, sender=model_class)
def create_signal_handler(name, bonus_point_setting):
def signal_handler(sender, instance, created, **kwargs):
"""
Signal handler for post-save events of the model instances.
"""
# request = getattr(_thread_locals, "request", None)
# previous_record = getattr(_thread_locals, "previous_record", None)
# previous_instance = None
# if previous_record:
# previous_instance = previous_record["instance"]
# if BonusPointSetting.objects.filter(model='Task').exists():
# bonus_point_settings = BonusPointSetting.objects.filter(model='Task')
# for bs in bonus_point_settings:
field_1 = date.today()
field_2 = instance.end_date
if bonus_point_setting.bonus_for == instance.status :
for employee in instance.task_members.all():
bonus_point_setting.create_employee_bonus(employee,field_1,field_2)
signal_handler.__name__ = name
signal_handler.model_class = model_class
signal_handler.bonus_point_setting = bonus_point_setting
return signal_handler
# Create and connect the signal handler
handler_name = f"{bonus_point_setting.id}_signal_handler"
dynamic_signal_handler = create_signal_handler(
handler_name, bonus_point_setting
)
SIGNAL_HANDLERS.append(dynamic_signal_handler)
post_save.connect(
dynamic_signal_handler, sender=dynamic_signal_handler.model_class
)
def create_pre_bulk_update_handler(automation, model_class):
def pre_bulk_update_handler(sender, queryset, *args, **kwargs):
request = getattr(_thread_locals, "request", None)
if request:
queryset_copy = queryset.none()
if queryset.count():
queryset_copy = QuerySet.from_list(copy.deepcopy(list(queryset)))
_thread_locals.previous_bulk_record = {
"automation": automation,
"queryset": queryset,
"queryset_copy": queryset_copy,
}
func_name = f"{automation.method_title}_pre_bulk_signal_handler"
# Dynamically create a function with a unique name
handler = types.FunctionType(
pre_bulk_update_handler.__code__,
globals(),
name=func_name,
argdefs=pre_bulk_update_handler.__defaults__,
closure=pre_bulk_update_handler.__closure__,
)
# Set additional attributes on the function
handler.model_class = model_class
handler.automation = automation
return handler
def track_previous_instance():
"""
method to add signal to track the automations model previous instances
"""
def clear_instance_signal_connection():
"""
Method to clear instance handler signals
"""
for handler in INSTANCE_HANDLERS:
pre_save.disconnect(handler, sender=handler.model_class)
pre_bulk_update.disconnect(handler, sender=handler.model_class)
INSTANCE_HANDLERS.clear()
clear_instance_signal_connection()
bonus_point_settings = BonusPointSetting.objects.filter(is_active=True)
for bonus_setting in bonus_point_settings:
model_class = get_model_class(bonus_setting.model)
# handler = create_pre_bulk_update_handler(bonus_setting, model_class)
# INSTANCE_HANDLERS.append(handler)
# pre_bulk_update.connect(handler, sender=model_class)
@receiver(pre_save, sender=model_class)
def instance_handler(sender, instance, **kwargs):
"""
Signal handler for pres-save events of the model instances.
"""
# prevented storing the scheduled activities
request = getattr(_thread_locals, "request", None)
if instance.pk:
# to get the previous instance
instance = model_class.objects.filter(id=instance.pk).first()
if request:
_thread_locals.previous_record = {
"bonus_setting": bonus_setting,
"instance": instance,
}
instance_handler.__name__ = (
f"{bonus_setting.id}_instance_handler"
)
return instance_handler
instance_handler.model_class = model_class
instance_handler.bonus_setting = bonus_setting
INSTANCE_HANDLERS.append(instance_handler)
track_previous_instance()
start_connection()

View File

@@ -0,0 +1,44 @@
<div id="formContainer">
{% include "generic/horilla_form.html" %}
</div>
{% comment %} {% load i18n %}
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="objectCreateModalLabel"
>{% trans "Bonus Point Settings" %}</span
>
<button
class="oh-modal__close--custom"
onclick="$(this).closest('.oh-modal--show').removeClass('oh-modal--show');"
aria-label="Close"
{% if close_hx_url and messages %}
hx-get="{{close_hx_url}}"
hx-target="{{close_hx_target}}"
{% endif %}
>
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="formBody">
<form hx-post="{% url 'create-bonus-setting' %}?instance_id={{form.instance.id}}" hx-target="#objectCreateModalTarget">
{{form.as_p}}
</form>
</div>
<script>
<!-- $(document).ready(function () {
$("#id_employee_id").closest(".col-md-6").removeClass("col-md-6")
$("#id_employee_id").on("change", function () {
var employees = $(this).find("option:selected");
values = $("#id_answer_employees").val();
$("#id_answer_employees").empty();
employees.each(function () {
var value = $(this).val();
var text = $(this).text();
var option = `<option value="${value}"> ${text}</option>`;
$("#id_answer_employees").append(option);
});
$("#id_answer_employees").val(values).change();
});
}); -->
</script> {% endcomment %}

View File

@@ -0,0 +1,49 @@
{% extends 'settings.html' %}
{% block settings %}
{% load i18n %} {% load static %}
{% for path in style_path %}
<link rel="stylesheet" href="{{path}}"/>
{% endfor %}
{% for path in script_static_paths %}
<script src="{{path}}"></script>
{% endfor %}
{% include "generic/components.html" %}
<div
class="oh-checkpoint-badge mb-2"
id="selectedInstances"
data-ids="[]"
data-clicked=""
style="display: none"
>
</div>
<div
hx-get="{{nav_url}}?{{request.GET.urlencode}}"
hx-trigger="load"
>
<div
class="mt-5 oh-wrapper animated-background"
style="height:80px;"
>
</div>
</div>
<div
class="oh-wrapper"
hx-get="{{view_url}}?{{request.GET.urlencode}}"
hx-trigger="load"
id="{{view_container_id}}"
>
<div
class="mt-4 animated-background"
style="height:600px;"
>
</div>
</div>
{% endblock settings %}

View File

@@ -0,0 +1,27 @@
{% load i18n %}
<div onclick="event.stopPropagation();" class="oh-btn-group">
{% if perms.leave.change_holiday %}
<button
class="oh-btn oh-btn--light-bkg w-100"
title="{% trans 'Edit' %}"
data-toggle="oh-modal-toggle"
data-target="#genericModal"
hx-get="{% url 'update-bonus-point-setting' instance.id %}?{{pd}}"
hx-target="#genericModalBody"
>
<ion-icon name="create-outline"></ion-icon>
</button>
{% endif %}
{% if perms.leave.delete_holiday %}
<a
class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
id="delete-link"
hx-confirm="{% trans 'Do you want to delete the bonus point setting?' %}"
hx-post="{% url 'delete-bonus-point-setting' instance.id %}"
hx-target="#listContainer"
title="{% trans 'Delete' %}"
>
<ion-icon name="trash-outline"></ion-icon>
</a>
{% endif %}
</div>

View File

@@ -0,0 +1,50 @@
{% extends 'index.html' %}
{% block content %}
{% load i18n %} {% load static %}
{% for path in style_path %}
<link rel="stylesheet" href="{{path}}"/>
{% endfor %}
{% for path in script_static_paths %}
<script src="{{path}}"></script>
{% endfor %}
{% include "generic/components.html" %}
<div
class="oh-checkpoint-badge mb-2"
id="selectedInstances"
data-ids="[]"
data-clicked=""
style="display: none"
>
</div>
<div
hx-get="{{nav_url}}?{{request.GET.urlencode}}"
hx-trigger="load"
>
<div
class="mt-5 oh-wrapper animated-background"
style="height:80px;"
>
</div>
</div>
<div
class="oh-wrapper"
hx-get="{{view_url}}?{{request.GET.urlencode}}"
hx-trigger="load"
id="{{view_container_id}}"
>
<div
class="mt-4 animated-background"
style="height:600px;"
>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,52 @@
{% extends 'settings.html' %} {% load i18n %} {% block settings %} {% load static %}
<div class="oh-inner-sidebar-content">
{% if perms.base.view_department %}
<div
class="oh-inner-sidebar-content__header d-flex justify-content-between align-items-center"
>
<h2 class="oh-inner-sidebar-content__title">{% trans "Bonus Point Setting" %}</h2>
{% if perms.base.add_department %}
<button
class="oh-btn oh-btn--secondary oh-btn--shadow"
data-toggle="oh-modal-toggle"
data-target="#departmentModal"
hx-get="{% url 'create-bonus-setting' %}"
hx-target="#departmentForm"
>
<ion-icon name="add-outline" class="me-1"></ion-icon>
{% trans "Create" %}
</button>
{% endif %}
</div>
{% if bonous_settings %}
<!-- {% include 'base/department/department_view.html' %} -->
{% else %}
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%;">
<img style="display: block; width: 15%; margin: 20px auto; filter: opacity(0.5);" src="{% static 'images/ui/coins.png' %}" class="" alt="Page not found. 404." />
<h5 class="oh-404__subtitle">{% trans "There is no Bonus setting at this moment." %}</h5>
</div>
{% endif %}
{% endif %}
</div>
<div
class="oh-modal"
id="departmentModal"
role="dialog"
aria-labelledby="departmentModalLabel"
aria-hidden="true"
>
<div class="oh-modal__dialog" id="departmentForm"></div>
</div>
<div
class="oh-modal"
id="departmentEditModal"
role="dialog"
aria-labelledby="departmentEditModal"
aria-hidden="true"
>
<div class="oh-modal__dialog" id="departmentEditForm"></div>
</div>
{% endblock settings %}

View File

@@ -1,6 +1,7 @@
from django.urls import path
from base.views import object_delete
from pms import cbvs
from . import models, views
@@ -415,4 +416,61 @@ urlpatterns = [
views.dashboard_feedback_answer,
name="dashboard-feedback-answer",
),
# ===========bonus point setting============
# path(
# "view-bonus-setting",
# views.view_bonus_setting,
# name="view-bonus-setting",
# ),
path(
"bonus-point-setting",
cbvs.BonusPointSettingSectionView.as_view(),
name="bonus-point-setting",
),
path(
"bonus-point-setting-nav",
cbvs.BonusPointSettingNavView.as_view(),
name="bonus-point-setting-nav",
),
path(
"create-bonus-point-setting",
cbvs.BonusPointSettingFormView.as_view(),
name="create-bonus-point-setting",
),
path(
"update-bonus-point-setting/<int:pk>/",
cbvs.BonusPointSettingFormView.as_view(),
name="update-bonus-point-setting",
),
path(
"delete-bonus-point-setting/<int:pk>/",
views.delete_bonus_point_setting,
name="delete-bonus-point-setting",
),
path(
"bonus-point-setting-list-view",
cbvs.BonusPointSettingListView.as_view(),
name="bonus-point-setting-list-view",
),
# ===========Employee bonus point============
path(
"employee-bonus-point",
cbvs.EmployeeBonusPointSectionView.as_view(),
name="employee-bonus-point",
),
path(
"employee-bonus-point-nav",
cbvs.EmployeeBonusPointNavView.as_view(),
name="employee-bonus-point-nav",
),
path(
"create-employee-bonus-point",
cbvs.EmployeeBonusPointFormView.as_view(),
name="create-employee-bonus-point",
),
path(
"employee-bonus-point-list-view",
cbvs.EmployeeBonusPointListView.as_view(),
name="employee-bonus-point-list-view",
),
]

View File

@@ -67,6 +67,7 @@ from pms.methods import (
from pms.models import (
AnonymousFeedback,
Answer,
BonusPointSetting,
Comment,
EmployeeKeyResult,
EmployeeObjective,
@@ -3575,3 +3576,18 @@ def dashboard_feedback_answer(request):
"request_and_approve/feedback_answer.html",
{"feedbacks": feedbacks, "current_date": datetime.date.today()},
)
@login_required
@permission_required("pms.delete_bonuspointsetting")
def delete_bonus_point_setting(request, pk):
"""
Automation delete view
"""
try:
BonusPointSetting.objects.get(id=pk).delete()
messages.success(request, "Bonus Point Setting deleted")
except Exception as e:
print(e)
messages.error(request, "Something went wrong")
return redirect(reverse("bonus-point-setting-list-view"))

BIN
static/images/ui/coins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -445,6 +445,33 @@
</li>
<hr>
{% endif %}
{% if "pms"|app_installed %}
<li class="oh-inner-sidebar__item">
<div class="oh-accordion ms-2">
<div class="oh-accordion-header" style="font-size:16px;">
<ion-icon name="analytics-outline" class="me-2"></ion-icon>
{% trans "Performnce" %}
</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="oh-input-group">
{% if perms.pms.view_payslipautogenerate %}
<a
id="autoGeneratePayslip"
href="{% url 'bonus-point-setting' %}"
class="oh-inner-sidebar__link oh-dropdown__link"
>{% trans "Bonus Point Setting" %}</a
>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</li>
<hr>
{% endif %}
{% if "helpdesk"|app_installed %}
<li class="oh-inner-sidebar__item">
<div class="oh-accordion ms-2">