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 %}
+
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 %}
+
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()