From f2037f516775604055ce56f4b78836279c9c6475 Mon Sep 17 00:00:00 2001 From: Horilla Date: Mon, 22 Jan 2024 19:17:12 +0530 Subject: [PATCH] [ADD] EMPLOYEE: Bonus point system and view inside employee profile view --- base/templatetags/basefilters.py | 8 ++ employee/admin.py | 3 +- employee/forms.py | 17 +++ employee/models.py | 54 ++++++++ .../employee/profile/profile_view.html | 17 +++ .../templates/employee/view/individual.html | 19 +++ employee/templates/policies/attachments.html | 2 +- employee/templates/tabs/bonus_points.html | 112 ++++++++++++++++ employee/templates/tabs/forms/add_points.html | 40 ++++++ .../tabs/forms/redeem_points_form.html | 29 ++++ employee/urls.py | 3 + employee/views.py | 126 ++++++++++++++++-- horilla_audit/methods.py | 5 +- payroll/forms/component_forms.py | 16 ++- payroll/models/models.py | 36 ++++- .../templates/payroll/reimbursement/form.html | 2 +- .../payroll/reimbursement/request_cards.html | 9 ++ .../reimbursement/view_reimbursement.html | 33 +++-- payroll/views/component_views.py | 3 + 19 files changed, 501 insertions(+), 33 deletions(-) create mode 100644 employee/templates/tabs/bonus_points.html create mode 100644 employee/templates/tabs/forms/add_points.html create mode 100644 employee/templates/tabs/forms/redeem_points_form.html diff --git a/base/templatetags/basefilters.py b/base/templatetags/basefilters.py index 7da59b8ec..e29c7bf83 100644 --- a/base/templatetags/basefilters.py +++ b/base/templatetags/basefilters.py @@ -108,3 +108,11 @@ def user_perms(perms): permission names return method """ return json.dumps(list(perms.values_list("codename", flat="True"))) + + +@register.filter(name="abs_value") +def abs_value(value): + """ + permission names return method + """ + return abs(value) \ No newline at end of file diff --git a/employee/admin.py b/employee/admin.py index 4472e0185..25b5d620a 100644 --- a/employee/admin.py +++ b/employee/admin.py @@ -5,6 +5,7 @@ This page is used to register the model with admins site. """ from django.contrib import admin from employee.models import ( + BonusPoint, Employee, EmployeeWorkInformation, EmployeeBankDetails, @@ -21,4 +22,4 @@ from simple_history.admin import SimpleHistoryAdmin admin.site.register(Employee) admin.site.register(EmployeeBankDetails) admin.site.register(EmployeeWorkInformation, SimpleHistoryAdmin) -admin.site.register([EmployeeNote, EmployeeTag, PolicyMultipleFile, Policy]) +admin.site.register([EmployeeNote, EmployeeTag, PolicyMultipleFile, Policy, BonusPoint]) diff --git a/employee/forms.py b/employee/forms.py index 77ba87692..d9a8af711 100644 --- a/employee/forms.py +++ b/employee/forms.py @@ -28,6 +28,7 @@ from django.forms import DateInput, TextInput from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as trans from employee.models import ( + BonusPoint, Employee, EmployeeWorkInformation, EmployeeBankDetails, @@ -478,3 +479,19 @@ class PolicyForm(ModelForm): if commit: instance.attachments.add(*multiple_attachment_ids) return instance, attachemnts + + +class BonusPointAddForm(ModelForm): + + class Meta: + model = BonusPoint + fields = ["points", "reason"] + widgets = { + 'reason': forms.TextInput(attrs={'required': 'required'}), + } + +class BonusPointRedeemForm(ModelForm): + class Meta: + model = BonusPoint + fields = ["points"] + \ No newline at end of file diff --git a/employee/models.py b/employee/models.py index 46c2cdd55..c2fb2e9f8 100644 --- a/employee/models.py +++ b/employee/models.py @@ -7,10 +7,14 @@ This module is used to register models for employee app import datetime as dtime from datetime import date, datetime import json +import threading +import time from typing import Any from django.conf import settings from django.db import models from django.contrib.auth.models import User, Permission +from django.dispatch import receiver +from django.db.models.signals import post_save, pre_delete from django.utils.translation import gettext_lazy as trans from django.utils.translation import gettext as _ from django.core.exceptions import ValidationError @@ -574,3 +578,53 @@ class Policy(models.Model): def delete(self, *args, **kwargs): super().delete(*args, **kwargs) self.attachments.all().delete() + + +class BonusPoint(models.Model): + CONDITIONS =[ + ('==',_('equals')), + ('>',_('grater than')), + ('<',_('less than')), + ('>=',_('greater than or equal')), + ('<=',_('less than or equal')), + ] + employee_id = models.OneToOneField(Employee, on_delete=models.PROTECT ,blank=True, null=True, related_name='bonus_point') + points = models.IntegerField(default=0, help_text="Use negative numbers to reduce points.") + encashment_condition = models.CharField(max_length=100,choices = CONDITIONS,blank=True, null=True) + redeeming_points = models.IntegerField(blank=True, null=True) + reason = models.TextField(blank=True, null=True) + history = HorillaAuditLog( + related_name="history_set", + bases=[ + HorillaAuditInfo, + ], + ) + + def __str__(self): + return f"{self.employee_id} - {self.points} Points" + + def tracking(self): + """ + This method is used to return the tracked history of the instance + """ + return get_diff(self) + + @receiver(post_save, sender=Employee) + def bonus_post_save(sender, instance, **_kwargs): + if not BonusPoint.objects.filter(employee_id__id = instance.id).exists(): + BonusPoint.objects.create( + employee_id = instance + ) + +class BonusPointThreading(threading.Thread): + + def run(self): + time.sleep(5) + employees = Employee.objects.all() + for employee in employees: + if not BonusPoint.objects.filter(employee_id__id = employee.id).exists(): + BonusPoint.objects.create( + employee_id = employee + ) + +BonusPointThreading().start() \ No newline at end of file diff --git a/employee/templates/employee/profile/profile_view.html b/employee/templates/employee/profile/profile_view.html index 74b6fab4c..f08119f2a 100644 --- a/employee/templates/employee/profile/profile_view.html +++ b/employee/templates/employee/profile/profile_view.html @@ -214,6 +214,17 @@ >{% trans "Performance" %} +
  • + {% trans "Bonus Points" %} +
  • {% include 'tabs/payroll-tab.html' %}
    +
    + {% include "tabs/bonus_points.html" %} +
    {% endif %} + {% if perms.employee.view_employeenote or request.user|check_manager:employee %} +
  • + {% trans "Bonus Points" %} +
  • + {% endif %}
    +
    + {% include "tabs/bonus_points.html" %} +
    + {% csrf_token %}
    + + + + diff --git a/employee/templates/tabs/forms/add_points.html b/employee/templates/tabs/forms/add_points.html new file mode 100644 index 000000000..ffd3845e9 --- /dev/null +++ b/employee/templates/tabs/forms/add_points.html @@ -0,0 +1,40 @@ +{% load i18n %} +
    + {% csrf_token %} +
    +
    + +
    + {{form.points}} {{form.points.errors}} +
    +
    +
    + +
    + {{form.reason}} {{form.reason.errors}} +
    +
    +
    +
    + +
    +
    diff --git a/employee/templates/tabs/forms/redeem_points_form.html b/employee/templates/tabs/forms/redeem_points_form.html new file mode 100644 index 000000000..229fbd520 --- /dev/null +++ b/employee/templates/tabs/forms/redeem_points_form.html @@ -0,0 +1,29 @@ +{% load i18n %} +
    + {% csrf_token %} +
    +
    + +
    + {{form.points}} {{form.points.errors}} +
    +
    +
    +
    + +
    +
    diff --git a/employee/urls.py b/employee/urls.py index 6f97f86ab..3c8f3fe15 100644 --- a/employee/urls.py +++ b/employee/urls.py @@ -211,6 +211,9 @@ urlpatterns = [ name="contract-tab", kwargs={"model": Employee}, ), + path("bonus-points-tab/", views.bonus_points_tab, name="bonus-points-tab"), + path("add-bonus-points/", views.add_bonus_points, name="add-bonus-points"), + path("redeem-points/", views.redeem_points, name="redeem-points"), path("employee-select/", views.employee_select, name="employee-select"), path( "employee-select-filter/", diff --git a/employee/views.py b/employee/views.py index 97b44cad4..36cdfa0eb 100755 --- a/employee/views.py +++ b/employee/views.py @@ -69,6 +69,8 @@ from base.methods import ( ) from employee.filters import EmployeeFilter, EmployeeReGroup from employee.forms import ( + BonusPointAddForm, + BonusPointRedeemForm, BulkUpdateFieldForm, EmployeeExportExcelForm, EmployeeForm, @@ -79,9 +81,15 @@ from employee.forms import ( EmployeeBankDetailsUpdateForm, excel_columns, ) -from employee.models import Employee, EmployeeNote, EmployeeWorkInformation, EmployeeBankDetails +from employee.models import ( + BonusPoint, + Employee, + EmployeeNote, + EmployeeWorkInformation, + EmployeeBankDetails, +) from payroll.methods.payslip_calc import dynamic_attr -from payroll.models.models import Allowance, Contract, Deduction +from payroll.models.models import Allowance, Contract, Deduction, Reimbursement from pms.models import Feedback from recruitment.models import Candidate @@ -445,7 +453,7 @@ def allowances_deductions_tab(request, emp_id): "active_contracts": active_contracts, "allowances": employee_allowances if employee_allowances else None, "deductions": employee_deductions if employee_deductions else None, - "employee":employee, + "employee": employee, } return render(request, "tabs/allowance_deduction-tab.html", context=context) @@ -1100,7 +1108,7 @@ def employee_filter_view(request): previous_data = request.GET.urlencode() field = request.GET.get("field") queryset = Employee.objects.filter() - employees = EmployeeFilter(request.GET,queryset=queryset).qs + employees = EmployeeFilter(request.GET, queryset=queryset).qs if request.GET.get("is_active") != "False": employees = employees.filter(is_active=True) page_number = request.GET.get("page") @@ -1342,7 +1350,9 @@ def employee_archive(request, obj_id): messages.success(request, message) else: related_models = ", ".join(model for model in result.get("related_models")) - messages.warning(request, _(f"Can't archive.Employee assigned as {related_models}")) + messages.warning( + request, _(f"Can't archive.Employee assigned as {related_models}") + ) return HttpResponseRedirect(request.META.get("HTTP_REFERER")) @@ -2132,6 +2142,7 @@ def employee_select_filter(request): return JsonResponse(context) + @login_required @manager_can_enter(perm="employee.view_employeenote") def note_tab(request, emp_id): @@ -2147,7 +2158,7 @@ def note_tab(request, emp_id): """ # employee = Employee.objects.get(id=emp_id) employee_obj = Employee.objects.get(id=emp_id) - notes = EmployeeNote.objects.filter(employee_id = emp_id) + notes = EmployeeNote.objects.filter(employee_id=emp_id) return render( request, @@ -2173,9 +2184,7 @@ def add_note(request, emp_id=None): note.updated_by = request.user.employee_get note.save() messages.success(request, _("Note added successfully..")) - response = render( - request, "tabs/add_note.html", {"form": form} - ) + response = render(request, "tabs/add_note.html", {"form": form}) return HttpResponse( response.content.decode("utf-8") + "" ) @@ -2221,6 +2230,7 @@ def employee_note_update(request, note_id): }, ) + @login_required @manager_can_enter(perm="employee.delete_employeenote") def employee_note_delete(request, note_id): @@ -2235,3 +2245,101 @@ def employee_note_delete(request, note_id): messages.success(request, _("Note deleted successfully...")) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + +@login_required +@manager_can_enter(perm="employee.view_bonuspoint") +def bonus_points_tab(request, emp_id): + """ + This function is used to view performance tab of an employee in employee individual & profile view. + + Parameters: + request (HttpRequest): The HTTP request object. + emp_id (int): The id of the employee. + + Returns: return note-tab template + + """ + employee_obj = Employee.objects.get(id=emp_id) + points = BonusPoint.objects.get(employee_id=emp_id) + trackings = points.tracking() + + activity_list = [] + for history in trackings: + activity_list.append( + { + "type":history["type"], + "date": history["pair"][0].history_date, + "points": history["pair"][0].points - history["pair"][1].points, + "user":getattr(User.objects.filter(id = history["pair"][0].history_user_id).first(),"employee_get",None), + "reason": history["pair"][0].reason, + } + ) + + + return render( + request, + "tabs/bonus_points.html", + {"employee": employee_obj, "points": points, "activity_list": activity_list}, + ) + + +@login_required +@manager_can_enter(perm="employee.add_bonuspoint") +def add_bonus_points(request, emp_id): + bonus_point = BonusPoint.objects.get(employee_id=emp_id) + form = BonusPointAddForm() + if request.method == "POST": + form = BonusPointAddForm( + request.POST, + request.FILES, + ) + if form.is_valid(): + form.save(commit=False) + bonus_point.points += form.cleaned_data["points"] + bonus_point.reason = form.cleaned_data["reason"] + bonus_point.save() + messages.success( + request, + _("Added {} points to the bonus account").format( + form.cleaned_data["points"] + ), + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + return render( + request, + "tabs/forms/add_points.html", + { + "form": form, + "emp_id": emp_id, + }, + ) + +@login_required +@owner_can_enter("employee.view_bonuspoint", Employee) +def redeem_points(request,emp_id): + user = Employee.objects.get(id=emp_id) + form = BonusPointRedeemForm() + if request.method == 'POST': + form = BonusPointRedeemForm(request.POST) + if form.is_valid(): + form.save(commit=False) + points = form.cleaned_data['points'] + reimbursement = Reimbursement.objects.create( + title = f"Bonus point Redeem for {user}", + type = "bonus_encashment", + employee_id = user, + bonus_to_encash =points, + description = f"{user} want to redeem {points} points", + allowance_on = date.today(), + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + return render( + request, + "tabs/forms/redeem_points_form.html", + { + "form": form, + "employee": user, + }, + ) + diff --git a/horilla_audit/methods.py b/horilla_audit/methods.py index fd2a5d736..e23bd4eee 100644 --- a/horilla_audit/methods.py +++ b/horilla_audit/methods.py @@ -106,7 +106,10 @@ def get_diff(instance): } ) if create_history: - updated_by = create_history.history_user.employee_get + try: + updated_by = create_history.history_user.employee_get + except: + updated_by = Bot() delta_changes.append( { "type": f"{create_history.instance.__class__._meta.verbose_name.capitalize()} created", diff --git a/payroll/forms/component_forms.py b/payroll/forms/component_forms.py index 06ee27c4f..178825265 100644 --- a/payroll/forms/component_forms.py +++ b/payroll/forms/component_forms.py @@ -477,7 +477,7 @@ class MultipleFileField(forms.FileField): result = [single_file_clean(d, initial) for d in data] else: result = [single_file_clean(data, initial)] - return result[0] + return result[0] if result else None class ReimbursementForm(ModelForm): @@ -535,7 +535,7 @@ class ReimbursementForm(ModelForm): type = self.data["type"] elif self.instance is not None: type = self.instance.type - + print(type) if not request.user.has_perm("payroll.add_reimbursement"): exclude_fields.append("employee_id") @@ -544,11 +544,21 @@ class ReimbursementForm(ModelForm): "leave_type_id", "cfd_to_encash", "ad_to_encash", + "bonus_to_encash", ] - elif self.instance.pk or self.data.get("type") == "leave_encashment": + elif self.instance.pk and type == "leave_encashment" or self.data.get("type") == "leave_encashment": exclude_fields = exclude_fields + [ "attachment", "amount", + "bonus_to_encash", + ] + elif self.instance.pk and type == "bonus_encashment" or self.data.get("type") == "bonus_encashment": + exclude_fields = exclude_fields + [ + "attachment", + "amount", + "leave_type_id", + "cfd_to_encash", + "ad_to_encash", ] if self.instance.pk: exclude_fields = exclude_fields + ["type", "employee_id"] diff --git a/payroll/models/models.py b/payroll/models/models.py index ac7989954..50e239057 100644 --- a/payroll/models/models.py +++ b/payroll/models/models.py @@ -18,7 +18,7 @@ from django.db.models.signals import pre_save, pre_delete from django.http import QueryDict from asset.models import Asset from base import thread_local_middleware -from employee.models import EmployeeWorkInformation +from employee.models import BonusPoint, EmployeeWorkInformation from employee.models import Employee, Department, JobPosition from base.models import Company, EmployeeShift, WorkType, JobRole from base.horilla_company_manager import HorillaCompanyManager @@ -1497,6 +1497,7 @@ class Reimbursement(models.Model): reimbursement_types = [ ("reimbursement", "Reimbursement"), ("leave_encashment", "Leave Encashment"), + ("bonus_encashment", "Bonus Point Encashment"), ] status_types = [ ("requested", "Requested"), @@ -1526,6 +1527,11 @@ class Reimbursement(models.Model): help_text="Carry Forward Days to encash", verbose_name="Carry forward days", ) + bonus_to_encash = models.IntegerField( + default=0, + help_text="Bonus points to encash", + verbose_name="Bonus points", + ) amount = models.FloatField(default=0) status = models.CharField( max_length=10, choices=status_types, default="requested", editable=False @@ -1556,16 +1562,34 @@ class Reimbursement(models.Model): raise ValidationError({"attachment": "This field is required"}) elif self.type == "leave_encashment" and self.leave_type_id is None: raise ValidationError({"leave_type_id": "This field is required"}) - self.cfd_to_encash = max((round(self.cfd_to_encash * 2) / 2), 0) - self.ad_to_encash = max((round(self.ad_to_encash * 2) / 2), 0) - assigned_leave = self.leave_type_id.employee_available_leave.filter( - employee_id=self.employee_id - ).first() + if self.type == "leave_encashment": + self.cfd_to_encash = max((round(self.cfd_to_encash * 2) / 2), 0) + self.ad_to_encash = max((round(self.ad_to_encash * 2) / 2), 0) + assigned_leave = self.leave_type_id.employee_available_leave.filter( + employee_id=self.employee_id + ).first() if self.status != "approved" or self.allowance_id is None: super().save(*args, **kwargs) if self.status == "approved" and self.allowance_id is None: if self.type == "reimbursement": proceed = True + elif self.type == "bonus_encashment": + proceed = False + bonus_points = BonusPoint.objects.get(employee_id=self.employee_id) + if bonus_points.points >= self.bonus_to_encash: + proceed = True + bonus_points.points -= self.bonus_to_encash + bonus_points.reason = "bonus points has been redeemed." + bonus_points.save() + else: + request = getattr( + thread_local_middleware._thread_locals, "request", None + ) + if request: + messages.info( + request, + "The employee don't have that much bonus points to encash.", + ) else: proceed = False if assigned_leave: diff --git a/payroll/templates/payroll/reimbursement/form.html b/payroll/templates/payroll/reimbursement/form.html index 02d214321..1759a678d 100644 --- a/payroll/templates/payroll/reimbursement/form.html +++ b/payroll/templates/payroll/reimbursement/form.html @@ -3,7 +3,7 @@ hx-encoding="multipart/form-data"> {{form.as_p}} - diff --git a/payroll/templates/payroll/reimbursement/request_cards.html b/payroll/templates/payroll/reimbursement/request_cards.html index 211de3312..12aa8c1cf 100644 --- a/payroll/templates/payroll/reimbursement/request_cards.html +++ b/payroll/templates/payroll/reimbursement/request_cards.html @@ -80,6 +80,15 @@

    + {% elif req.type == 'bonus_encashment' %} +

    + + {% trans 'Requsted for' %} {{ req.bonus_to_encash }} + {% trans 'Bonus points to encash.' %} + + +

    {% else %}

    diff --git a/payroll/templates/payroll/reimbursement/view_reimbursement.html b/payroll/templates/payroll/reimbursement/view_reimbursement.html index 47467148d..021263a9e 100644 --- a/payroll/templates/payroll/reimbursement/view_reimbursement.html +++ b/payroll/templates/payroll/reimbursement/view_reimbursement.html @@ -42,21 +42,32 @@ if (element.val() == 'reimbursement') { $('#reimbursementModalBody [name=attachment]').parent().show() $('#reimbursementModalBody [name=attachment]').attr("required",true) - - $('#reimbursementModalBody [name=leave_type_id]').parent().hide().attr("required",false) - $('#reimbursementModalBody [name=cfd_to_encash]').parent().hide().attr("required",false) - $('#reimbursementModalBody [name=ad_to_encash]').parent().hide().attr("required",false) - $('#reimbursementModalBody [name=amount]').parent().show().attr("required",true) - $('#reimbursementModalBody #availableTable').hide().attr("required",false) + $('#reimbursementModalBody [name=leave_type_id]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=cfd_to_encash]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=ad_to_encash]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=amount]').parent().show().attr("required",true) + $('#reimbursementModalBody #availableTable').hide().attr("required",false) + $('#reimbursementModalBody [name=bonus_to_encash]').parent().hide().attr("required",false) } else if(element.val() == 'leave_encashment') { $('#reimbursementModalBody [name=attachment]').parent().hide() $('#reimbursementModalBody [name=attachment]').attr("required",false) - $('#reimbursementModalBody [name=leave_type_id]').parent().show().attr("required",true) - $('#reimbursementModalBody [name=cfd_to_encash]').parent().show().attr("required",true) - $('#reimbursementModalBody [name=ad_to_encash]').parent().show().attr("required",true) - $('#reimbursementModalBody [name=amount]').parent().hide().attr("required",false) - $('#reimbursementModalBody #availableTable').show().attr("required",true) + $('#reimbursementModalBody [name=leave_type_id]').parent().show().attr("required",true) + $('#reimbursementModalBody [name=cfd_to_encash]').parent().show().attr("required",true) + $('#reimbursementModalBody [name=ad_to_encash]').parent().show().attr("required",true) + $('#reimbursementModalBody [name=amount]').parent().hide().attr("required",false) + $('#reimbursementModalBody #availableTable').show().attr("required",true) + $('#reimbursementModalBody [name=bonus_to_encash]').parent().hide().attr("required",false) + + } else if(element.val() == 'bonus_encashment') { + $('#reimbursementModalBody [name=attachment]').parent().hide() + $('#reimbursementModalBody [name=attachment]').attr("required",false) + $('#reimbursementModalBody [name=leave_type_id]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=cfd_to_encash]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=ad_to_encash]').parent().hide().attr("required",false) + $('#reimbursementModalBody [name=amount]').parent().hide().attr("required",false) + $('#reimbursementModalBody #availableTable').hide().attr("required",false) + $('#reimbursementModalBody [name=bonus_to_encash]').parent().show().attr("required",true) } diff --git a/payroll/views/component_views.py b/payroll/views/component_views.py index 9d650f3e0..d17cb7f01 100644 --- a/payroll/views/component_views.py +++ b/payroll/views/component_views.py @@ -1099,6 +1099,9 @@ def approve_reimbursements(request): for reimbursement in reimbursements: if reimbursement.type == "leave_encashment": reimbursement.amount = amount + elif reimbursement.type == "bonus_encashment" : + reimbursement.amount = amount + emp = reimbursement.employee_id reimbursement.status = status reimbursement.save()