[ADD] EMPLOYEE: Initial configuration for company policies

This commit is contained in:
Horilla
2024-01-19 09:42:49 +05:30
parent 59da606380
commit 6ab3eb4544
13 changed files with 474 additions and 16 deletions

View File

@@ -4,7 +4,15 @@ admin.py
This page is used to register the model with admins site.
"""
from django.contrib import admin
from employee.models import Employee, EmployeeWorkInformation, EmployeeBankDetails, EmployeeNote, EmployeeTag
from employee.models import (
Employee,
EmployeeWorkInformation,
EmployeeBankDetails,
EmployeeNote,
EmployeeTag,
PolicyMultipleFile,
Policy,
)
from simple_history.admin import SimpleHistoryAdmin
@@ -13,4 +21,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])
admin.site.register([EmployeeNote, EmployeeTag, PolicyMultipleFile, Policy])

View File

@@ -4,6 +4,7 @@ filters.py
This page is used to register filter for employee models
"""
from employee.models import Policy
import uuid
from django import forms
import django_filters
@@ -84,7 +85,7 @@ class EmployeeFilter(FilterSet):
"employee_work_info__reporting_manager_id",
"employee_work_info__company_id",
"employee_work_info__shift_id",
"employee_work_info__tags"
"employee_work_info__tags",
]
def not_in_yet_func(self, queryset, _, value):
@@ -196,3 +197,15 @@ class EmployeeReGroup:
("employee_work_info.reporting_manager_id", "Reporting Manager"),
("employee_work_info.company_id", "Company"),
]
class PolicyFilter(FilterSet):
"""
PolicyFilter filterset class
"""
search = django_filters.CharFilter(field_name="title", lookup_expr="icontains")
class Meta:
model = Policy
fields = "__all__"

View File

@@ -27,7 +27,14 @@ from django.contrib.auth.models import User
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 Employee, EmployeeWorkInformation, EmployeeBankDetails, EmployeeNote
from employee.models import (
Employee,
EmployeeWorkInformation,
EmployeeBankDetails,
EmployeeNote,
Policy,
PolicyMultipleFile,
)
from base.methods import reload_queryset
@@ -405,12 +412,69 @@ class EmployeeNoteForm(ModelForm):
"""
model = EmployeeNote
exclude = (
"updated_by",
)
exclude = ("updated_by",)
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
field = self.fields["employee_id"]
field.widget = field.hidden_widget()
field.widget = field.hidden_widget()
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = [single_file_clean(data, initial)]
if len(result) == 0:
result = [[]]
return result[0]
class PolicyForm(ModelForm):
"""
PolicyForm
"""
class Meta:
model = Policy
fields = "__all__"
exclude = ["attachments"]
widgets = {
"body": forms.Textarea(
attrs={"data-summernote": "", "style": "display:none;"}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attachment"] = MultipleFileField(
label="Attachements", required=False
)
def save(self, *args, commit=True, **kwargs):
attachemnt = []
multiple_attachment_ids = []
attachemnts = None
if self.files.getlist("attachment"):
attachemnts = self.files.getlist("attachment")
multiple_attachment_ids = []
for attachemnt in attachemnts:
file_instance = PolicyMultipleFile()
file_instance.attachment = attachemnt
file_instance.save()
multiple_attachment_ids.append(file_instance.pk)
instance = super().save(commit)
if commit:
instance.attachments.add(*multiple_attachment_ids)
return instance, attachemnts

View File

@@ -7,6 +7,7 @@ This module is used to register models for employee app
import datetime as dtime
from datetime import date, datetime
import json
from typing import Any
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User, Permission
@@ -211,7 +212,7 @@ class Employee(models.Model):
"""
today = datetime.today()
attendance = self.employee_attendances.filter(attendance_date=today).first()
minimum_hour_seconds = strtime_seconds(getattr(attendance,"minimum_hour","0"))
minimum_hour_seconds = strtime_seconds(getattr(attendance, "minimum_hour", "0"))
at_work = 0
forecasted_pending_hours = 0
if attendance:
@@ -546,3 +547,30 @@ class EmployeeNote(models.Model):
def __str__(self) -> str:
return f"{self.description}"
class PolicyMultipleFile(models.Model):
"""
PoliciesMultipleFile model
"""
attachment = models.FileField(upload_to="employee/policies")
class Policy(models.Model):
"""
Policies model
"""
title = models.CharField(max_length=50)
body = models.TextField()
is_visible_to_all = models.BooleanField(default=True)
specific_employees = models.ManyToManyField(Employee, blank=True,editable=False)
attachments = models.ManyToManyField(PolicyMultipleFile, blank=True)
company_id = models.ManyToManyField(Company, blank=True)
objects = HorillaCompanyManager()
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self.attachments.all().delete()

127
employee/policies.py Normal file
View File

@@ -0,0 +1,127 @@
"""
policies.py
This module is used to write operation related to policies
"""
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import render
from base.views import paginator_qry
from employee.filters import PolicyFilter
from employee.forms import PolicyForm
from employee.models import Policy, PolicyMultipleFile
from horilla.decorators import permission_required, login_required
@login_required
def view_policies(request):
"""
Method is used render template to view all the policy records
"""
policies = Policy.objects.all()
if not request.user.has_perm("employee.view_policy"):
policies = policies.filter(is_visible_to_all=True)
return render(
request,
"policies/view_policies.html",
{"policies": paginator_qry(policies, request.GET.get("page"))},
)
@login_required
@permission_required("employee.add_policy")
def create_policy(request):
"""
Method is used to create/update new policy
"""
instance_id = request.GET.get("instance_id")
instance = None
if isinstance(eval(str(instance_id)), int):
instance = Policy.objects.filter(id=instance_id).first()
form = PolicyForm(instance=instance)
if request.method == "POST":
form = PolicyForm(request.POST, request.FILES, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Policy saved")
return HttpResponse("<script>window.location.reload()</script>")
return render(request, "policies/form.html", {"form": form})
@login_required
def search_policies(request):
"""
This method is used to search in policies
"""
policies = PolicyFilter(request.GET).qs
if not request.user.has_perm("employee.view_policy"):
policies = policies.filter(is_visible_to_all=True)
return render(
request,
"policies/records.html",
{
"policies": paginator_qry(policies, request.GET.get("page")),
"pd": request.GET.urlencode(),
},
)
@login_required
def view_policy(request):
"""
This method is used to view the policy
"""
instance_id = request.GET["instance_id"]
policy = Policy.objects.filter(id=instance_id).first()
return render(
request,
"policies/view_policy.html",
{
"policy": policy,
},
)
@login_required
@permission_required("employee.change_policy")
def add_attachment(request):
"""
This method is used to add attachment to policy
"""
files = request.FILES.getlist("files")
policy_id = request.GET["policy_id"]
attachments = []
for file in files:
attachment = PolicyMultipleFile()
attachment.attachment = file
attachment.save()
attachments.append(attachment)
policy = Policy.objects.get(id=policy_id)
policy.attachments.add(*attachments)
messages.success(request, "Attachments added")
return render(request, "policies/attachments.html", {"policy": policy})
@login_required
@permission_required("employee.delete_policy")
def remove_attachment(request):
"""
This method is used to remove the attachments
"""
ids = request.GET.getlist("ids")
policy_id = request.GET["policy_id"]
policy = Policy.objects.get(id=policy_id)
PolicyMultipleFile.objects.filter(id__in=ids).delete()
return render(request, "policies/attachments.html", {"policy": policy})
@login_required
def get_attachments(request):
"""
This method is used to view all the attachments inside the policy
"""
policy = request.GET["policy_id"]
policy = Policy.objects.get(id=policy)
return render(request, "policies/attachments.html", {"policy": policy})

View File

@@ -0,0 +1,26 @@
{% load static %}
<form hx-post="{% url 'add-attachment-policy' %}?policy_id={{policy.id}}" hx-target="#attachmentContainer" class="add-files-form d-flex" method="post"
hx-encoding="multipart/form-data">
{% for attachment in policy.attachments.all %}
<a href="{{ attachment.attachment.url }}" rel="noopener noreferrer" target="_blank"><span
class="oh-file-icon oh-file-icon--pdf" onmouseover="enlargeImage('{{ attachment.attachment.url }}')"
style="width:40px;height:40px"><img src="{% static '/images/ui/minus-icon.png' %}"
style="display:block;width:50%;height:50%"
hx-get="{% url 'remove-attachment-policy' %}?ids={{ attachment.id }}&policy_id={{ policy.id }}"
hx-target="#attachmentContainer"
onclick="event.stopPropagation();event.preventDefault()" />
</span>
</a>
{% endfor %}
{% if perms.employee.add_policy %}
<input type="hidden" name="csrfmiddlewaretoken"/>
<input type="file" name="files" class="d-none" multiple="true" id="addFile_18" onchange="submitForm(this)" />
<input type="submit" class="d-none add_more_submit" value="save" />
<label for="addFile_18" title="Add Files" onclick="console.log($(this).closest('[type=file]'));"
style="cursor: pointer;">
<ion-icon name="add-outline" style="font-size: 24px" role="img" class="md hydrated"
aria-label="add outline"></ion-icon>
</label>
{% endif %}
</form>

View File

View File

@@ -0,0 +1,7 @@
{% load i18n %}
<form hx-post="{% url 'create-policy' %}?instance_id={{form.instance.id}}" hx-encoding="multipart/form-data">
{{ form.as_p }}
<div class="d-flex flex-row-reverse">
<button type="submit" class="oh-btn oh-btn--secondary">{% trans 'Save' %}</button>
</div>
</form>

View File

@@ -0,0 +1,34 @@
{% load i18n %}
<section class="oh-wrapper oh-main__topbar" style="padding-bottom: 1rem;">
<div class="oh-main__titlebar oh-main__titlebar--left oh-d-flex-column--resp oh-mb-3--small">
<h1 class="oh-main__titlebar-title fw-bold">{% trans 'Policies' %}</h1>
</div>
<div class="oh-main__titlebar oh-main__titlebar--right oh-d-flex-column--resp oh-mb-3--small">
<div class="oh-input-group oh-input__search-group mr-4">
<ion-icon name="search-outline" class="oh-input-group__icon oh-input-group__icon--left md hydrated" role="img" aria-label="search outline"></ion-icon>
<input hx-get="{% url 'search-policies' %}" name="search" hx-trigger="keyup changed delay:.2s" hx-target="#policyContainer" type="text" placeholder="Search" style="margin-right:10px" class="oh-input oh-input__icon mr-3" autocomplete="false" aria-label="Search Input" />
</div>
{% include 'policies/filter.html' %}
{% if perms.payroll.add_policyaccount %}
<div class="oh-main__titlebar-button-container">
<div class="oh-main__titlebar-button-container">
<a hx-get="{% url 'create-policy' %}" hx-target="#policyModalBody" data-toggle="oh-modal-toggle" data-target="#policyModal" class="oh-btn oh-btn--secondary">
<ion-icon name="add-outline"></ion-icon>
{% trans 'Create' %}
</a>
</div>
</div>
{% endif %}
</div>
</section>
<div class="oh-modal" id="policyModal" role="dialog" aria-hidden="true">
<div class="oh-modal__dialog" style="max-width: 550px">
<div class="oh-modal__dialog-header">
<button type="button" class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
</div>
<div class="oh-modal__dialog-body" id="policyModalBody"></div>
</div>
</div>

View File

@@ -0,0 +1,98 @@
{% load i18n %}
<div class="oh-faq-cards">
{% for policy in policies %}
<div class="oh-faq-card">
<h3 class="oh-faq-card__title d-flex justify-content-between">
<span>
{% if policy.is_visible_to_all %}
<span class="oh-dot oh-dot--small me-1" style="background-color:yellowgreen"></span>
{% else %}
<span class="oh-dot oh-dot--small me-1" style="background-color:gray"></span>
{% endif %}
{{ policy.title }}</span>
<div>
{% if perms.employee.change_policiy %}
<a hx-get="{% url 'create-policy' %}?instance_id={{ policy.id }}" hx-target="#policyModalBody" data-toggle="oh-modal-toggle" data-target="#policyModal" title="Edit" style="cursor: pointer;"><ion-icon name="create-outline" role="img" class="md hydrated" aria-label="copy outline"></ion-icon></a>
{% endif %}
{% if perms.employee.delete_policy %}
<a href="/recruitment/delete-mail-template/?ids=1" class="text-danger" style="cursor: pointer;" onclick="return confirm('Do you want to delete this policy?')" title="Delete"><ion-icon name="trash-outline" role="img" class="md hydrated" aria-label="trash outline"></ion-icon></a>
{% endif %}
</div>
</h3>
<p class="oh-card__footer--border-top">
<div style="max-height: 350px;overflow: hidden;" class="truncated-text">{{ policy.body|safe }}</div>
</p>
{% if perms.recruitment.change_recruitmentmailtemplate %}
<a hx-get="{% url 'view-policy' %}?instance_id={{ policy.id }}" hx-target="#policyModalBody" data-toggle="oh-modal-toggle" data-target="#policyModal" class="oh-btn oh-btn--secondary oh-btn--block">{% trans 'View policy' %}</a>
{% endif %}
</div>
{% endfor %}
</div>
<div class="oh-wrapper w-100">
<div class="oh-pagination">
<span class="oh-pagination__page" data-toggle="modal" data-target="#policyContainer">{% trans 'Page' %} {{ policies.number }} {% trans 'of' %} {{ policies.paginator.num_pages }}.</span>
<nav class="oh-pagination__nav">
<div class="oh-pagination__input-container me-3">
<span class="oh-pagination__label me-1">{% trans 'Page' %}</span>
<input type="number" name="page" class="oh-pagination__input" value="{{ policies.number }}" hx-get="{% url 'search-policies' %}?{{ pd }}" hx-target="#policyContainer" min="1" />
<span class="oh-pagination__label">{% trans 'of' %} {{ policies.paginator.num_pages }}</span>
</div>
<ul class="oh-pagination__items">
{% if policies.has_previous %}
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target="#policyContainer" hx-get="{% url 'search-policies' %}?{{ pd }}&page=1" class="oh-pagination__link">{% trans 'First' %}</a>
</li>
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target="#policyContainer" hx-get="{% url 'search-policies' %}?{{ pd }}&page={{ policies.previous_page_number }}" class="oh-pagination__link">{% trans 'Previous' %}</a>
</li>
{% endif %}
{% if policies.has_next %}
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target="#policyContainer" hx-get="{% url 'search-policies' %}?{{ pd }}&page={{ policies.next_page_number }}" class="oh-pagination__link">{% trans 'Next' %}</a>
</li>
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target="#policyContainer" hx-get="{% url 'search-policies' %}?{{ pd }}&page={{ policies.paginator.num_pages }}" class="oh-pagination__link">{% trans 'Last' %}</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
<script>
function submitForm(elem) {
$(elem).siblings('.add_more_submit').click()
}
function enlargeImage(src) {
var enlargeImageContainer = $('#enlargeImageContainer')
enlargeImageContainer.empty()
style = 'width:100%; height:90%; box-shadow: 0 10px 10px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); background:white'
var enlargedImage = $('<iframe>').attr({ src: src, style: style })
var name = $('<span>').text(src.split('/').pop().replace(/_/g, ' '))
enlargeImageContainer.append(enlargedImage)
enlargeImageContainer.append(name)
setTimeout(function () {
enlargeImageContainer.show()
const iframe = document.querySelector('iframe').contentWindow
var iframe_document = iframe.document
iframe_image = iframe_document.getElementsByTagName('img')[0]
$(iframe_image).attr('style', 'width:100%; height:100%;')
}, 100)
}
function hideEnlargeImage() {
var enlargeImageContainer = $('#enlargeImageContainer')
enlargeImageContainer.empty()
}
$(document).on('click', function (event) {
if (!$(event.target).closest('#enlargeImageContainer').length) {
hideEnlargeImage()
}
})
</script>

View File

@@ -0,0 +1,7 @@
{% extends 'index.html' %}
{% block content %}
{% include 'policies/nav.html' %}
<div class="oh-wrapper" id="policyContainer">
{% include 'policies/records.html' %}
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<style>
#enlargeImageContainer {
position: absolute;
left: -300px;
top: 100px;
height: 200px;
width: 200px;
}
</style>
<h2>{{ policy.title }}</h2>
<p class="oh-card__footer--border-top"></p>
{{ policy.body|safe }}
<div id="attachmentContainer" class="d-flex" hx-get="{% url 'get-attachments-policy' %}?policy_id={{ policy.id }}" hx-trigger="load"></div>
<div id="enlargeImageContainer"></div>

View File

@@ -4,7 +4,7 @@ urls.py
This module is used to map url path with view methods.
"""
from django.urls import path
from employee import not_in_out_dashboard, views
from employee import not_in_out_dashboard, policies, views
from employee.models import Employee
urlpatterns = [
@@ -188,10 +188,22 @@ urlpatterns = [
path("note-tab/<int:emp_id>", views.note_tab, name="note-tab"),
path("add-employee-note/<int:emp_id>/", views.add_note, name="add-employee-note"),
path("add-employee-note-post", views.add_note, name="add-employee-note-post"),
path("employee-note-update/<int:note_id>/", views.employee_note_update, name="employee-note-update"),
path("employee-note-delete/<int:note_id>/", views.employee_note_delete, name="employee-note-delete"),
path(
"employee-note-update/<int:note_id>/",
views.employee_note_update,
name="employee-note-update",
),
path(
"employee-note-delete/<int:note_id>/",
views.employee_note_delete,
name="employee-note-delete",
),
path("attendance-tab/<int:emp_id>", views.attendance_tab, name="attendance-tab"),
path("allowances-deductions-tab/<int:emp_id>", views.allowances_deductions_tab, name="allowances-deductions-tab"),
path(
"allowances-deductions-tab/<int:emp_id>",
views.allowances_deductions_tab,
name="allowances-deductions-tab",
),
path("shift-tab/<int:emp_id>", views.shift_tab, name="shift-tab"),
path(
"contract-tab/<int:obj_id>",
@@ -207,7 +219,26 @@ urlpatterns = [
),
path("not-in-yet/", not_in_out_dashboard.not_in_yet, name="not-in-yet"),
path("not-out-yet/", not_in_out_dashboard.not_out_yet, name="not-out-yet"),
path("send-mail/<int:emp_id>/", not_in_out_dashboard.send_mail, name="send-mail-employee"),
path("send-mail", not_in_out_dashboard.send_mail_to_employee, name="send-mail-to-employee"),
path("get-template/<int:emp_id>/", not_in_out_dashboard.get_template, name="get-template-employee"),
path(
"send-mail/<int:emp_id>/",
not_in_out_dashboard.send_mail,
name="send-mail-employee",
),
path(
"send-mail",
not_in_out_dashboard.send_mail_to_employee,
name="send-mail-to-employee",
),
path(
"get-template/<int:emp_id>/",
not_in_out_dashboard.get_template,
name="get-template-employee",
),
path("view-policies", policies.view_policies, name="view-policies"),
path("search-policies", policies.search_policies, name="search-policies"),
path("create-policy", policies.create_policy, name="create-policy"),
path("view-policy", policies.view_policy, name="view-policy"),
path("add-attachment-policy", policies.add_attachment, name="add-attachment-policy"),
path("remove-attachment-policy", policies.remove_attachment, name="remove-attachment-policy"),
path("get-attachments-policy", policies.get_attachments, name="get-attachments-policy"),
]