From 7ef7852d0ee9626c6502333c3dfc81cc827504f355e158d8f4d7dcf06bebc85f Mon Sep 17 00:00:00 2001 From: nestict Date: Fri, 16 Jan 2026 15:38:54 +0100 Subject: [PATCH] Upload files to "base" Signed-off-by: nestict --- base/.DS_Store | Bin 0 -> 8196 bytes base/__init__.py | 1 + base/admin.py | 72 + base/announcement.py | 416 ++ base/apps.py | 38 + base/backends.py | 267 ++ base/context_processors.py | 301 ++ base/countries.py | 855 ++++ base/decorators.py | 68 + base/filters.py | 404 ++ base/forms.py | 2800 ++++++++++++ base/horilla_company_manager.py | 112 + base/methods.py | 1154 +++++ base/middleware.py | 249 + base/models.py | 1865 ++++++++ base/request_and_approve.py | 56 + base/scheduler.py | 501 ++ base/signals.py | 242 + base/tests.py | 3 + base/threading.py | Bin 0 -> 1024 bytes base/translator.py | 364 ++ base/urls.py | 1077 +++++ base/views.py | 7553 +++++++++++++++++++++++++++++++ 23 files changed, 18398 insertions(+) create mode 100644 base/.DS_Store create mode 100644 base/__init__.py create mode 100644 base/admin.py create mode 100644 base/announcement.py create mode 100644 base/apps.py create mode 100644 base/backends.py create mode 100644 base/context_processors.py create mode 100644 base/countries.py create mode 100644 base/decorators.py create mode 100644 base/filters.py create mode 100644 base/forms.py create mode 100644 base/horilla_company_manager.py create mode 100644 base/methods.py create mode 100644 base/middleware.py create mode 100644 base/models.py create mode 100644 base/request_and_approve.py create mode 100644 base/scheduler.py create mode 100644 base/signals.py create mode 100644 base/tests.py create mode 100644 base/threading.py create mode 100644 base/translator.py create mode 100644 base/urls.py create mode 100644 base/views.py diff --git a/base/.DS_Store b/base/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..95e08e8934ced9e738e136a94dffdca4fc07d7e6ec32b82b60b84deb0da68a84 GIT binary patch literal 8196 zcmeHM!EVz)5S>k2>a-wnqZfq52UM!~fl$%|H>7?5q=}nYvg3%PAa1@R{sajjet{3* zg7|?-Js>3DfP^@}o7q+CU9X!SDpaYv(e60*d$XRmV>{yzk=i{P?+|SfQH;*^_9g~T zfet%@0zrYGKu{nk5EQry3gDT|EpB=4>!V=|3IqkN zr2>3^NYL5#jjgnmTL%Uy0bnESmWFNA0m4a)?HgNZD^&Q@)q^rrWl9XC={TM+9JX(4 zrLCru(sWX0W@RcAC9{K<2%J=3+b{+Nf&xtixOU%^nmwbLRlmP_oR_16ygUGcFRF*= z+kd5Di(d&0X-ow;GpY9-yt?b)*MXnov+$x{JLp^RozjR(dJf)Fte^SYVQ$bN&M5~c z1=q{zOdL)O4$oAy;6FR~4Iqbzdq@+^QlQUh>FD+DBAk4(vp*^e^R^^<502M&Q4R)X zE#l+7Plw-reB||L!HzGColooY%e|9*rciGRMaF(TBp2S+a)CJ+w9spnfjOgvtw;Bf zM~?=_)&mS3V*^*$gQ!aH|2);hgFVw@71q{6=I=@}m@4Lj<>bofGk*>yD@sh+FOS}t=-W8AjIB~Ai5pusJKh`n_9n%Y3Eh)Y9 zdtdwF;PExIpunXo(A6Hdxc=X3fB%2!^9F}OfuO+EQ9wn5{lPBI$JEx1Gviu2L4StM zjd7K>Lct*AIINW8u%~|*VmkqpIelX*ZLtUKzrP4L`&DLs!-VI*EJPTtl>+|&t}vlO literal 0 HcmV?d00001 diff --git a/base/__init__.py b/base/__init__.py new file mode 100644 index 0000000..aba122a --- /dev/null +++ b/base/__init__.py @@ -0,0 +1 @@ +from . import scheduler diff --git a/base/admin.py b/base/admin.py new file mode 100644 index 0000000..d332ff9 --- /dev/null +++ b/base/admin.py @@ -0,0 +1,72 @@ +""" +admin.py + +This page is used to register base models with admins site. +""" + +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin + +from base.models import ( + Announcement, + Attachment, + Company, + CompanyLeaves, + DashboardEmployeeCharts, + Department, + DynamicEmailConfiguration, + DynamicPagination, + EmailLog, + EmployeeShift, + EmployeeShiftDay, + EmployeeShiftSchedule, + EmployeeType, + Holidays, + JobPosition, + JobRole, + MultipleApprovalCondition, + MultipleApprovalManagers, + PenaltyAccounts, + RotatingShift, + RotatingShiftAssign, + RotatingWorkType, + RotatingWorkTypeAssign, + ShiftRequest, + ShiftRequestComment, + Tags, + WorkType, + WorkTypeRequest, + WorkTypeRequestComment, +) + +# Register your models here. + +admin.site.register(Company) +admin.site.register(Department, SimpleHistoryAdmin) +admin.site.register(JobPosition) +admin.site.register(JobRole) +admin.site.register(EmployeeShift) +admin.site.register(EmployeeShiftSchedule) +admin.site.register(EmployeeShiftDay) +admin.site.register(EmployeeType) +admin.site.register(WorkType) +admin.site.register(RotatingWorkType) +admin.site.register(RotatingWorkTypeAssign) +admin.site.register(RotatingShift) +admin.site.register(RotatingShiftAssign) +admin.site.register(ShiftRequest) +admin.site.register(WorkTypeRequest) +admin.site.register(Tags) +admin.site.register(DynamicEmailConfiguration) +admin.site.register(MultipleApprovalManagers) +admin.site.register(ShiftRequestComment) +admin.site.register(WorkTypeRequestComment) +admin.site.register(DynamicPagination) +admin.site.register(Announcement) +admin.site.register(Attachment) +admin.site.register(EmailLog) +admin.site.register(DashboardEmployeeCharts) +admin.site.register(Holidays) +admin.site.register(CompanyLeaves) +admin.site.register(PenaltyAccounts) +admin.site.register(MultipleApprovalCondition) diff --git a/base/announcement.py b/base/announcement.py new file mode 100644 index 0000000..d6ea4ee --- /dev/null +++ b/base/announcement.py @@ -0,0 +1,416 @@ +""" +Module for managing announcements, including creation, updates, comments, and views. +""" + +import json +from datetime import datetime, timedelta + +from django.contrib import messages +from django.contrib.auth.models import User +from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from base.forms import AnnouncementCommentForm, AnnouncementForm +from base.methods import closest_numbers, filter_own_records +from base.models import ( + Announcement, + AnnouncementComment, + AnnouncementExpire, + AnnouncementView, + Attachment, +) +from employee.models import Employee +from horilla.decorators import hx_request_required, login_required, permission_required +from notifications.signals import notify + + +@login_required +@hx_request_required +def announcement_list(request): + """ + Renders a list of announcements for the authenticated user. + + This view fetches all announcements and updates their expiration dates if not already set. + It filters announcements based on the user's permissions and whether the announcements + are still valid (not expired). Additionally, it checks if the user has viewed each announcement. + """ + general_expire_date = ( + AnnouncementExpire.objects.values_list("days", flat=True).first() or 30 + ) + announcements = Announcement.objects.all() + announcements_to_update = [] + + for announcement in announcements.filter(expire_date__isnull=True): + announcement.expire_date = announcement.created_at + timedelta( + days=general_expire_date + ) + announcements_to_update.append(announcement) + + if announcements_to_update: + Announcement.objects.bulk_update(announcements_to_update, ["expire_date"]) + + has_view_permission = request.user.has_perm("base.view_announcement") + announcements = announcements.filter(expire_date__gte=datetime.today().date()) + announcement_items = ( + announcements + if has_view_permission + else announcements.filter( + Q(employees=request.user.employee_get) | Q(employees__isnull=True) + ) + ) + + filtered_announcements = announcement_items.prefetch_related( + "announcementview_set" + ).order_by("-created_at") + for announcement in filtered_announcements: + announcement.has_viewed = announcement.announcementview_set.filter( + user=request.user, viewed=True + ).exists() + instance_ids = json.dumps([instance.id for instance in filtered_announcements]) + context = { + "announcements": filtered_announcements, + "general_expire_date": general_expire_date, + "instance_ids": instance_ids, + } + return render(request, "announcement/announcements_list.html", context) + + +@login_required +@hx_request_required +def create_announcement(request): + """ + Create a new announcement and notify relevant users. + """ + form = AnnouncementForm() + if request.method == "POST": + form = AnnouncementForm(request.POST, request.FILES) + if form.is_valid(): + announcement, attachment_ids = form.save(commit=False) + announcement.save() + announcement.attachments.set(attachment_ids) + + employees = form.cleaned_data["employees"] + departments = form.cleaned_data["department"] + job_positions = form.cleaned_data["job_position"] + company = form.cleaned_data["company_id"] + + announcement.department.set(departments) + announcement.job_position.set(job_positions) + announcement.company_id.set(company) + + dept_ids = departments.values_list("id", flat=True) + job_ids = job_positions.values_list("id", flat=True) + + employees_from_dept = Employee.objects.filter( + employee_work_info__department_id__in=dept_ids + ) + employees_from_job = Employee.objects.filter( + employee_work_info__job_position_id__in=job_ids + ) + + all_employees = ( + employees | employees_from_dept | employees_from_job + ).distinct() + announcement.employees.add(*all_employees) + + all_emps = employees_from_dept | employees_from_job | employees + user_map = User.objects.filter(employee_get__in=all_emps).distinct() + + dept_emp_ids = set(employees_from_dept.values_list("id", flat=True)) + job_emp_ids = set(employees_from_job.values_list("id", flat=True)) + direct_emp_ids = set(employees.values_list("id", flat=True)) + + notified_ids = dept_emp_ids.union(job_emp_ids) + direct_only_ids = direct_emp_ids - notified_ids + + sender = request.user.employee_get + + def send_notification(users, verb): + if users.exists(): + notify.send( + sender, + recipient=users, + verb=verb, + verb_ar="لقد تم ذكرك في إعلان.", + verb_de="Sie wurden in einer Ankündigung erwähnt.", + verb_es="Has sido mencionado en un anuncio.", + verb_fr="Vous avez été mentionné dans une annonce.", + redirect="/", + icon="chatbox-ellipses", + ) + + send_notification( + user_map.filter(employee_get__id__in=dept_emp_ids), + _("Your department was mentioned in an announcement."), + ) + send_notification( + user_map.filter(employee_get__id__in=job_emp_ids), + _("Your job position was mentioned in an announcement."), + ) + send_notification( + user_map.filter(employee_get__id__in=direct_only_ids), + _("You have been mentioned in an announcement."), + ) + + messages.success(request, _("Announcement created successfully.")) + form = AnnouncementForm() # Reset the form + + return render(request, "announcement/announcement_form.html", {"form": form}) + + +@login_required +@hx_request_required +def delete_announcement(request, anoun_id): + """ + This method is used to delete announcements. + """ + announcement = Announcement.find(anoun_id) + if announcement: + announcement.delete() + messages.success(request, _("Announcement deleted successfully.")) + + instance_ids = request.GET.get("instance_ids") + instance_ids_list = json.loads(instance_ids) + __, next_instance_id = ( + closest_numbers(instance_ids_list, anoun_id) + if instance_ids_list + else (None, None) + ) + + if anoun_id in instance_ids_list: + instance_ids_list.remove(anoun_id) + + if next_instance_id and next_instance_id != anoun_id: + url = reverse("announcement-single-view", kwargs={"anoun_id": next_instance_id}) + return redirect(f"{url}?instance_ids={json.dumps(instance_ids_list)}") + return redirect(announcement_single_view) + + +@login_required +@hx_request_required +def update_announcement(request, anoun_id): + """ + This method renders form and template to update Announcement + """ + + announcement = Announcement.objects.get(id=anoun_id) + form = AnnouncementForm(instance=announcement) + existing_attachments = list(announcement.attachments.all()) + + instance_ids = request.GET.get("instance_ids") + + if request.method == "POST": + form = AnnouncementForm(request.POST, request.FILES, instance=announcement) + if form.is_valid(): + anou, attachment_ids = form.save(commit=False) + anou.save() + if attachment_ids: + all_attachments = set(existing_attachments) | set( + Attachment.objects.filter(id__in=attachment_ids) + ) + anou.attachments.set(all_attachments) + else: + anou.attachments.set(existing_attachments) + + employees = form.cleaned_data["employees"] + departments = form.cleaned_data["department"] + job_positions = form.cleaned_data["job_position"] + company = form.cleaned_data["company_id"] + anou.department.set(departments) + anou.job_position.set(job_positions) + anou.company_id.set(company) + messages.success(request, _("Announcement updated successfully.")) + + emp_dep = User.objects.filter( + employee_get__employee_work_info__department_id__in=departments + ) + emp_jobs = User.objects.filter( + employee_get__employee_work_info__job_position_id__in=job_positions + ) + employees = employees | Employee.objects.filter( + employee_work_info__department_id__in=departments + ) + employees = employees | Employee.objects.filter( + employee_work_info__job_position_id__in=job_positions + ) + anou.employees.add(*employees) + + notify.send( + request.user.employee_get, + recipient=emp_dep, + verb="Your department was mentioned in a post.", + verb_ar="تم ذكر قسمك في منشور.", + verb_de="Ihr Abteilung wurde in einem Beitrag erwähnt.", + verb_es="Tu departamento fue mencionado en una publicación.", + verb_fr="Votre département a été mentionné dans un post.", + redirect="/", + icon="chatbox-ellipses", + ) + + notify.send( + request.user.employee_get, + recipient=emp_jobs, + verb="Your job position was mentioned in a post.", + verb_ar="تم ذكر وظيفتك في منشور.", + verb_de="Ihre Arbeitsposition wurde in einem Beitrag erwähnt.", + verb_es="Tu puesto de trabajo fue mencionado en una publicación.", + verb_fr="Votre poste de travail a été mentionné dans un post.", + redirect="/", + icon="chatbox-ellipses", + ) + return render( + request, + "announcement/announcement_update_form.html", + { + "form": form, + "instance_ids": instance_ids, + "hx_target": request.META.get("HTTP_HX_TARGET", ""), + }, + ) + + +@login_required +@hx_request_required +def remove_announcement_file(request, obj_id, attachment_id): + announcement = get_object_or_404(Announcement, id=obj_id) + attachment = get_object_or_404(Attachment, id=attachment_id) + + announcement.attachments.remove(attachment) + messages.success(request, _("The file has been successfully deleted.")) + return HttpResponse("") + + +@login_required +@hx_request_required +def create_announcement_comment(request, anoun_id): + """ + This method renders form and template to create Announcement comments + """ + anoun = Announcement.objects.filter(id=anoun_id).first() + emp = request.user.employee_get + form = AnnouncementCommentForm( + initial={"employee_id": emp.id, "request_id": anoun_id} + ) + comments = AnnouncementComment.objects.filter(announcement_id=anoun_id) + commentators = [] + if comments: + for i in comments: + commentators.append(i.employee_id.employee_user_id) + unique_users = list(set(commentators)) + + if request.method == "POST": + form = AnnouncementCommentForm(request.POST) + if form.is_valid(): + form.instance.employee_id = emp + form.instance.announcement_id = anoun + form.save() + form = AnnouncementCommentForm( + initial={"employee_id": emp.id, "request_id": anoun_id} + ) + messages.success(request, _("You commented a post.")) + notify.send( + request.user.employee_get, + recipient=unique_users, + verb=f"Comment under the announcement {anoun.title}.", + verb_ar=f"تعليق تحت الإعلان {anoun.title}.", + verb_de=f"Kommentar unter der Ankündigung {anoun.title}.", + verb_es=f"Comentario bajo el anuncio {anoun.title}.", + verb_fr=f"Commentaire sous l'annonce {anoun.title}.", + redirect="/", + icon="chatbox-ellipses", + ) + return redirect("announcement-view-comment", anoun_id=anoun_id) + + return render( + request, + "announcement/comment_view.html", + {"form": form, "request_id": anoun_id}, + ) + + +@login_required +@hx_request_required +def comment_view(request, anoun_id): + """ + This method is used to view all comments in the announcements + """ + announcement = Announcement.objects.get(id=anoun_id) + comments = AnnouncementComment.objects.filter(announcement_id=anoun_id).order_by( + "-created_at" + ) + if not announcement.public_comments: + comments = filter_own_records( + request, comments, "base.view_announcementcomment" + ) + no_comments = not comments.exists() + + return render( + request, + "announcement/comment_view.html", + { + "comments": comments, + "no_comments": no_comments, + "request_id": anoun_id, + "announcement": announcement, + }, + ) + + +@login_required +@hx_request_required +def delete_announcement_comment(request, comment_id): + """ + This method is used to delete announcement comments + """ + comment = AnnouncementComment.objects.get(id=comment_id) + comment.delete() + messages.success(request, _("Comment deleted successfully!")) + return HttpResponse() + + +@login_required +@hx_request_required +def announcement_single_view(request, anoun_id=None): + """ + This method is used to render single announcements. + """ + announcement_instance = Announcement.find(anoun_id) + instance_ids = request.GET.get("instance_ids") + instance_ids_list = json.loads(instance_ids) if instance_ids else [] + previous_instance_id, next_instance_id = ( + closest_numbers(instance_ids_list, anoun_id) + if instance_ids_list + else (None, None) + ) + if announcement_instance: + announcement_view_obj, _ = AnnouncementView.objects.get_or_create( + user=request.user, announcement=announcement_instance + ) + announcement_view_obj.viewed = True + announcement_view_obj.save() + + context = { + "announcement": announcement_instance, + "instance_ids": instance_ids, + "previous_instance_id": previous_instance_id, + "next_instance_id": next_instance_id, + } + + return render(request, "announcement/announcement_one.html", context) + + +@login_required +@hx_request_required +@permission_required("base.view_announcement") +def viewed_by(request): + """ + This method is used to view the employees + """ + announcement_id = request.GET.get("announcement_id") + viewed_users = AnnouncementView.objects.filter( + announcement_id__id=announcement_id, viewed=True + ) + return render(request, "announcement/viewed_by.html", {"viewed_by": viewed_users}) diff --git a/base/apps.py b/base/apps.py new file mode 100644 index 0000000..3d81716 --- /dev/null +++ b/base/apps.py @@ -0,0 +1,38 @@ +""" +This module contains the configuration for the 'base' app. +""" + +from django.apps import AppConfig + + +class BaseConfig(AppConfig): + """ + Configuration class for the 'base' app. + """ + + default_auto_field = "django.db.models.BigAutoField" + name = "base" + + def ready(self) -> None: + from base import signals + + super().ready() + try: + from base.models import EmployeeShiftDay + + if not EmployeeShiftDay.objects.exists(): + days = [ + ("monday", "Monday"), + ("tuesday", "Tuesday"), + ("wednesday", "Wednesday"), + ("thursday", "Thursday"), + ("friday", "Friday"), + ("saturday", "Saturday"), + ("sunday", "Sunday"), + ] + + EmployeeShiftDay.objects.bulk_create( + [EmployeeShiftDay(day=day[0]) for day in days] + ) + except Exception as e: + pass diff --git a/base/backends.py b/base/backends.py new file mode 100644 index 0000000..ac016a7 --- /dev/null +++ b/base/backends.py @@ -0,0 +1,267 @@ +""" +email_backend.py + +This module is used to write email backends +""" + +import importlib +import logging + +from django.core.cache import cache +from django.core.mail import EmailMessage +from django.core.mail.backends.smtp import EmailBackend + +from base.models import DynamicEmailConfiguration, EmailLog +from horilla import settings +from horilla.horilla_middlewares import _thread_locals + +logger = logging.getLogger(__name__) + + +class DefaultHorillaMailBackend(EmailBackend): + def __init__( + self, + host=None, + port=None, + username=None, + password=None, + use_tls=None, + fail_silently=None, + use_ssl=None, + timeout=None, + ssl_keyfile=None, + ssl_certfile=None, + **kwargs, + ): + self.configuration = self.get_dynamic_email_config() + ssl_keyfile = ( + getattr(self.configuration, "ssl_keyfile", None) + if self.configuration + else ssl_keyfile or getattr(settings, "ssl_keyfile", None) + ) + ssl_certfile = ( + getattr(self.configuration, "ssl_certfile", None) + if self.configuration + else ssl_certfile or getattr(settings, "ssl_certfile", None) + ) + + super().__init__( + host=self.dynamic_host, + port=self.dynamic_port, + username=self.dynamic_username, + password=self.dynamic_password, + use_tls=self.dynamic_use_tls, + fail_silently=self.dynamic_fail_silently, + use_ssl=self.dynamic_use_ssl, + timeout=self.dynamic_timeout, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **kwargs, + ) + + @staticmethod + def get_dynamic_email_config(): + request = getattr(_thread_locals, "request", None) + company = None + if request and not request.user.is_anonymous: + company = request.user.employee_get.get_company() + configuration = DynamicEmailConfiguration.objects.filter( + company_id=company + ).first() + if configuration is None: + configuration = DynamicEmailConfiguration.objects.filter( + is_primary=True + ).first() + if configuration: + display_email_name = ( + f"{configuration.display_name} <{configuration.from_email}>" + ) + + user_id = "" + if request: + if ( + configuration.use_dynamic_display_name + and request.user.is_authenticated + ): + display_email_name = f"{request.user.employee_get.get_full_name()} <{request.user.employee_get.get_email()}>" + if request.user.is_authenticated: + user_id = request.user.pk + reply_to = [ + f"{request.user.employee_get.get_full_name()} <{request.user.employee_get.get_email()}>", + ] + cache.set(f"reply_to{request.user.pk}", reply_to) + + cache.set(f"dynamic_display_name{user_id}", display_email_name) + + return configuration + + @property + def dynamic_host(self): + return ( + self.configuration.host + if self.configuration + else getattr(settings, "EMAIL_HOST", None) + ) + + @property + def dynamic_port(self): + return ( + self.configuration.port + if self.configuration + else getattr(settings, "EMAIL_PORT", None) + ) + + @property + def dynamic_username(self): + return ( + self.configuration.username + if self.configuration + else getattr(settings, "EMAIL_HOST_USER", None) + ) + + @property + def dynamic_mail_sent_from(self): + return ( + self.configuration.from_email + if self.configuration + else getattr(settings, "DEFAULT_FROM_EMAIL", None) + ) + + @property + def dynamic_display_name(self): + return self.configuration.display_name if self.configuration else None + + @property + def dynamic_from_email_with_display_name(self): + return ( + f"{self.dynamic_display_name} <{self.dynamic_mail_sent_from}>" + if self.dynamic_display_name + else self.dynamic_mail_sent_from + ) + + @property + def dynamic_password(self): + return ( + self.configuration.password + if self.configuration + else getattr(settings, "EMAIL_HOST_PASSWORD", None) + ) + + @property + def dynamic_use_tls(self): + return ( + self.configuration.use_tls + if self.configuration + else getattr(settings, "EMAIL_USE_TLS", None) + ) + + @property + def dynamic_fail_silently(self): + return ( + self.configuration.fail_silently + if self.configuration + else getattr(settings, "EMAIL_FAIL_SILENTLY", True) + ) + + @property + def dynamic_use_ssl(self): + return ( + self.configuration.use_ssl + if self.configuration + else getattr(settings, "EMAIL_USE_SSL", None) + ) + + @property + def dynamic_timeout(self): + return ( + self.configuration.timeout + if self.configuration + else getattr(settings, "EMAIL_TIMEOUT", None) + ) + + +EMAIL_BACKEND = getattr(settings, "EMAIL_BACKEND", "") + + +BACKEND_CLASS: EmailBackend = DefaultHorillaMailBackend +default = "base.backends.ConfiguredEmailBackend" + +setattr(BACKEND_CLASS, "send_messages", DefaultHorillaMailBackend.send_messages) + +if EMAIL_BACKEND and EMAIL_BACKEND != default: + module_path, class_name = EMAIL_BACKEND.rsplit(".", 1) + module = importlib.import_module(module_path) + BACKEND_CLASS = getattr(module, class_name) + + +class ConfiguredEmailBackend(BACKEND_CLASS): + + def send_messages(self, email_messages): + response = super(BACKEND_CLASS, self).send_messages(email_messages) + for message in email_messages: + email_log = EmailLog( + subject=message.subject, + from_email=self.dynamic_from_email_with_display_name, + to=message.to, + body=message.body, + status="sent" if response else "failed", + ) + email_log.save() + return response + + +if EMAIL_BACKEND != default: + from_mail = getattr(settings, "DEFAULT_FROM_EMAIL", "example@gmail.com") + username = getattr(settings, "EMAIL_HOST_USER", "example@gmail.com") + ConfiguredEmailBackend.dynamic_username = from_mail + ConfiguredEmailBackend.dynamic_from_email_with_display_name = from_mail + + +__all__ = ["ConfiguredEmailBackend"] + + +message_init = EmailMessage.__init__ + + +def new_init( + self, + subject="", + body="", + from_email=None, + to=None, + bcc=None, + connection=None, + attachments=None, + headers=None, + cc=None, + reply_to=None, +): + """ + custom __init_method to override + """ + request = getattr(_thread_locals, "request", None) + DefaultHorillaMailBackend() + user_id = "" + if request and request.user and request.user.is_authenticated: + user_id = request.user.pk + reply_to = cache.get(f"reply_to{user_id}") if not reply_to else reply_to + + if not from_email: + from_email = cache.get(f"dynamic_display_name{user_id}") + + message_init( + self, + subject=subject, + body=body, + from_email=from_email, + to=to, + bcc=bcc, + connection=connection, + attachments=attachments, + headers=headers, + cc=cc, + reply_to=reply_to, + ) + + +EmailMessage.__init__ = new_init diff --git a/base/context_processors.py b/base/context_processors.py new file mode 100644 index 0000000..803a90a --- /dev/null +++ b/base/context_processors.py @@ -0,0 +1,301 @@ +""" +context_processor.py + +This module is used to register context processor` +""" + +import re + +from django.apps import apps +from django.contrib import messages +from django.http import HttpResponse +from django.urls import path, reverse +from django.utils.translation import gettext_lazy as _ + +from base.models import Company, TrackLateComeEarlyOut +from base.urls import urlpatterns +from employee.models import ( + Employee, + EmployeeGeneralSetting, + EmployeeWorkInformation, + ProfileEditFeature, +) +from horilla import horilla_apps +from horilla.decorators import hx_request_required, login_required, permission_required +from horilla.methods import get_horilla_model_class + + +class AllCompany: + """ + Dummy class + """ + + class Urls: + url = "https://ui-avatars.com/api/?name=All+Company&background=random" + + company = "All Company" + icon = Urls() + text = "All companies" + id = None + + +def get_last_section(path): + # Remove any trailing slash and split the path + segments = path.strip("/").split("/") + + # Get the last section (the ID) + last_section = segments[-1] if segments else None + return last_section + + +def get_companies(request): + """ + This method will return the history additional field form + """ + companies = list( + [company.id, company.company, company.icon.url, False] + for company in Company.objects.all() + ) + companies = [ + [ + "all", + "All Company", + "https://ui-avatars.com/api/?name=All+Company&background=random", + False, + ], + ] + companies + selected_company = request.session.get("selected_company") + company_selected = False + if selected_company and selected_company == "all": + companies[0][3] = True + company_selected = True + else: + for company in companies: + if str(company[0]) == selected_company: + company[3] = True + company_selected = True + return {"all_companies": companies, "company_selected": company_selected} + + +@login_required +@hx_request_required +@permission_required("base.change_company") +def update_selected_company(request): + """ + This method is used to update the selected company on the session + """ + company_id = request.GET.get("company_id") + user = request.user.employee_get + user_company = getattr( + getattr(user, "employee_work_info", None), "company_id", None + ) + request.session["selected_company"] = company_id + company = ( + AllCompany() + if company_id == "all" + else ( + Company.objects.filter(id=company_id).first() + if Company.objects.filter(id=company_id).first() + else AllCompany() + ) + ) + previous_path = request.GET.get("next", "/") + # Define the regex pattern for the path + pattern = r"^/employee/employee-view/\d+/$" + # Check if the previous path matches the pattern + if company_id != "all": + if re.match(pattern, previous_path): + employee_id = get_last_section(previous_path) + employee = Employee.objects.filter(id=employee_id).first() + emp_company = getattr( + getattr(employee, "employee_work_info", None), "company_id", None + ) + if emp_company != company: + text = "Other Company" + if company_id == user_company: + text = "My Company" + company = { + "company": company.company, + "icon": company.icon.url, + "text": text, + "id": company.id, + } + messages.error( + request, _("Employee is not working in the selected company.") + ) + request.session["selected_company_instance"] = company + return HttpResponse( + f""" + + """ + ) + + if company_id == "all": + text = "All companies" + elif company_id == user_company: + text = "My Company" + else: + text = "Other Company" + + company = { + "company": company.company, + "icon": company.icon.url, + "text": text, + "id": company.id, + } + request.session["selected_company_instance"] = company + return HttpResponse("") + + +urlpatterns.append( + path( + "update-selected-company", + update_selected_company, + name="update-selected-company", + ) +) + + +def white_labelling_company(request): + white_labelling = getattr(horilla_apps, "WHITE_LABELLING", False) + if white_labelling: + hq = Company.objects.filter(hq=True).last() + try: + company = ( + request.user.employee_get.get_company() + if request.user.employee_get.get_company() + else hq + ) + except: + company = hq + + return { + "white_label_company_name": company.company if company else "Horilla", + "white_label_company": company, + } + else: + return { + "white_label_company_name": "Horilla", + "white_label_company": None, + } + + +def resignation_request_enabled(request): + """ + Check weather resignation_request enabled of not in offboarding + """ + enabled_resignation_request = False + first = None + if apps.is_installed("offboarding"): + OffboardingGeneralSetting = get_horilla_model_class( + app_label="offboarding", model="offboardinggeneralsetting" + ) + first = OffboardingGeneralSetting.objects.first() + if first: + enabled_resignation_request = first.resignation_request + return {"enabled_resignation_request": enabled_resignation_request} + + +def timerunner_enabled(request): + """ + Check weather resignation_request enabled of not in offboarding + """ + first = None + enabled_timerunner = True + if apps.is_installed("attendance"): + AttendanceGeneralSetting = get_horilla_model_class( + app_label="attendance", model="attendancegeneralsetting" + ) + first = AttendanceGeneralSetting.objects.first() + if first: + enabled_timerunner = first.time_runner + return {"enabled_timerunner": enabled_timerunner} + + +def intial_notice_period(request): + """ + Check weather resignation_request enabled of not in offboarding + """ + initial = 30 + first = None + if apps.is_installed("payroll"): + PayrollGeneralSetting = get_horilla_model_class( + app_label="payroll", model="payrollgeneralsetting" + ) + first = PayrollGeneralSetting.objects.first() + if first: + initial = first.notice_period + return {"get_initial_notice_period": initial} + + +def check_candidate_self_tracking(request): + """ + This method is used to get the candidate self tracking is enabled or not + """ + + candidate_self_tracking = False + if apps.is_installed("recruitment"): + RecruitmentGeneralSetting = get_horilla_model_class( + app_label="recruitment", model="recruitmentgeneralsetting" + ) + first = RecruitmentGeneralSetting.objects.first() + else: + first = None + if first: + candidate_self_tracking = first.candidate_self_tracking + return {"check_candidate_self_tracking": candidate_self_tracking} + + +def check_candidate_self_tracking_rating(request): + """ + This method is used to check enabled/disabled of rating option + """ + rating_option = False + if apps.is_installed("recruitment"): + RecruitmentGeneralSetting = get_horilla_model_class( + app_label="recruitment", model="recruitmentgeneralsetting" + ) + first = RecruitmentGeneralSetting.objects.first() + else: + first = None + if first: + rating_option = first.show_overall_rating + return {"check_candidate_self_tracking_rating": rating_option} + + +def get_initial_prefix(request): + """ + This method is used to get the initial prefix + """ + settings = EmployeeGeneralSetting.objects.first() + instance_id = None + prefix = "PEP" + if settings: + instance_id = settings.id + prefix = settings.badge_id_prefix + return {"get_initial_prefix": prefix, "prefix_instance_id": instance_id} + + +def biometric_app_exists(request): + from django.conf import settings + + biometric_app_exists = "biometric" in settings.INSTALLED_APPS + return {"biometric_app_exists": biometric_app_exists} + + +def enable_late_come_early_out_tracking(request): + tracking = TrackLateComeEarlyOut.objects.first() + enable = tracking.is_enable if tracking else True + return {"tracking": enable, "late_come_early_out_tracking": enable} + + +def enable_profile_edit(request): + from accessibility.accessibility import ACCESSBILITY_FEATURE + + profile_edit = ProfileEditFeature.objects.filter().first() + enable = True if profile_edit and profile_edit.is_enabled else False + if enable: + if not any(item[0] == "profile_edit" for item in ACCESSBILITY_FEATURE): + ACCESSBILITY_FEATURE.append(("profile_edit", _("Profile Edit Access"))) + + return {"profile_edit_enabled": enable} diff --git a/base/countries.py b/base/countries.py new file mode 100644 index 0000000..da6eafe --- /dev/null +++ b/base/countries.py @@ -0,0 +1,855 @@ +# Countries +country_arr = [ + "Afghanistan", + "Albania", + "Algeria", + "American Samoa", + "Angola", + "Anguilla", + "Antartica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Ashmore and Cartier Island", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Brazil", + "British Virgin Islands", + "Brunei", + "Bulgaria", + "Burkina Faso", + "Burma", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Cayman Islands", + "Central African Republic", + "Chad", + "Chile", + "China", + "Christmas Island", + "Clipperton Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo, Democratic Republic of the", + "Congo, Republic of the", + "Cook Islands", + "Costa Rica", + "Cote d'Ivoire", + "Croatia", + "Cuba", + "Cyprus", + "Czeck Republic", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Europa Island", + "Falkland Islands (Islas Malvinas)", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern and Antarctic Lands", + "Gabon", + "Gambia, The", + "Gaza Strip", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Glorioso Islands", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Heard Island and McDonald Islands", + "Holy See (Vatican City)", + "Honduras", + "Hong Kong", + "Howland Island", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran", + "Iraq", + "Ireland", + "Ireland, Northern", + "Israel", + "Italy", + "Jamaica", + "Jan Mayen", + "Japan", + "Jarvis Island", + "Jersey", + "Johnston Atoll", + "Jordan", + "Juan de Nova Island", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea, North", + "Korea, South", + "Kuwait", + "Kyrgyzstan", + "Laos", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macau", + "Macedonia, Former Yugoslav Republic of", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Man, Isle of", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia, Federated States of", + "Midway Islands", + "Moldova", + "Monaco", + "Mongolia", + "Montserrat", + "Morocco", + "Mozambique", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "Netherlands Antilles", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcaim Islands", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "Reunion", + "Romainia", + "Russia", + "Rwanda", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Pierre and Miquelon", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Scotland", + "Senegal", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia and South Sandwich Islands", + "Spain", + "Spratly Islands", + "Sri Lanka", + "Sudan", + "Suriname", + "Svalbard", + "Swaziland", + "Sweden", + "Switzerland", + "Syria", + "Taiwan", + "Tajikistan", + "Tanzania", + "Thailand", + "Tobago", + "Toga", + "Tokelau", + "Tonga", + "Trinidad", + "Tunisia", + "Turkey", + "Turkmenistan", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "Uruguay", + "USA", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Vietnam", + "Virgin Islands", + "Wales", + "Wallis and Futuna", + "West Bank", + "Western Sahara", + "Yemen", + "Yugoslavia", + "Zambia", + "Zimbabwe", +] + +# States +s_a = [] +s_a.append( + "Badakhshan|Badghis|Baghlan|Balkh|Bamian|Farah|Faryab|Ghazni|Ghowr|Helmand|Herat|Jowzjan|Kabol|Kandahar|Kapisa|Konar|Kondoz|Laghman|Lowgar|Nangarhar|Nimruz|Oruzgan|Paktia|Paktika|Parvan|Samangan|Sar-e Pol|Takhar|Vardak|Zabol" +) +s_a.append( + "Berat|Bulqize|Delvine|Devoll (Bilisht)|Diber (Peshkopi)|Durres|Elbasan|Fier|Gjirokaster|Gramsh|Has (Krume)|Kavaje|Kolonje (Erseke)|Korce|Kruje|Kucove|Kukes|Kurbin|Lezhe|Librazhd|Lushnje|Malesi e Madhe (Koplik)|Mallakaster (Ballsh)|Mat (Burrel)|Mirdite (Rreshen)|Peqin|Permet|Pogradec|Puke|Sarande|Shkoder|Skrapar (Corovode)|Tepelene|Tirane (Tirana)|Tirane (Tirana)|Tropoje (Bajram Curri)|Vlore" +) +s_a.append( + "Adrar|Ain Defla|Ain Temouchent|Alger|Annaba|Batna|Bechar|Bejaia|Biskra|Blida|Bordj Bou Arreridj|Bouira|Boumerdes|Chlef|Constantine|Djelfa|El Bayadh|El Oued|El Tarf|Ghardaia|Guelma|Illizi|Jijel|Khenchela|Laghouat|M'Sila|Mascara|Medea|Mila|Mostaganem|Naama|Oran|Ouargla|Oum el Bouaghi|Relizane|Saida|Setif|Sidi Bel Abbes|Skikda|Souk Ahras|Tamanghasset|Tebessa|Tiaret|Tindouf|Tipaza|Tissemsilt|Tizi Ouzou|Tlemcen" +) +s_a.append("Eastern|Manu'a|Rose Island|Swains Island|Western") +s_a.append( + "Andorra la Vella|Bengo|Benguela|Bie|Cabinda|Canillo|Cuando Cubango|Cuanza Norte|Cuanza Sul|Cunene|Encamp|Escaldes-Engordany|Huambo|Huila|La Massana|Luanda|Lunda Norte|Lunda Sul|Malanje|Moxico|Namibe|Ordino|Sant Julia de Loria|Uige|Zaire" +) +s_a.append("Anguilla") +s_a.append("Antartica") +s_a.append( + "Barbuda|Redonda|Saint George|Saint John|Saint Mary|Saint Paul|Saint Peter|Saint Philip" +) +s_a.append( + "Antartica e Islas del Atlantico Sur|Buenos Aires|Buenos Aires Capital Federal|Catamarca|Chaco|Chubut|Cordoba|Corrientes|Entre Rios|Formosa|Jujuy|La Pampa|La Rioja|Mendoza|Misiones|Neuquen|Rio Negro|Salta|San Juan|San Luis|Santa Cruz|Santa Fe|Santiago del Estero|Tierra del Fuego|Tucuman" +) +s_a.append( + "Aragatsotn|Ararat|Armavir|Geghark'unik'|Kotayk'|Lorri|Shirak|Syunik'|Tavush|Vayots' Dzor|Yerevan" +) +s_a.append("Aruba") +s_a.append("Ashmore and Cartier Island") +s_a.append( + "Australian Capital Territory|New South Wales|Northern Territory|Queensland|South Australia|Tasmania|Victoria|Western Australia" +) +s_a.append( + "Burgenland|Kaernten|Niederoesterreich|Oberoesterreich|Salzburg|Steiermark|Tirol|Vorarlberg|Wien" +) +s_a.append( + "Abseron Rayonu|Agcabadi Rayonu|Agdam Rayonu|Agdas Rayonu|Agstafa Rayonu|Agsu Rayonu|Ali Bayramli Sahari|Astara Rayonu|Baki Sahari|Balakan Rayonu|Barda Rayonu|Beylaqan Rayonu|Bilasuvar Rayonu|Cabrayil Rayonu|Calilabad Rayonu|Daskasan Rayonu|Davaci Rayonu|Fuzuli Rayonu|Gadabay Rayonu|Ganca Sahari|Goranboy Rayonu|Goycay Rayonu|Haciqabul Rayonu|Imisli Rayonu|Ismayilli Rayonu|Kalbacar Rayonu|Kurdamir Rayonu|Lacin Rayonu|Lankaran Rayonu|Lankaran Sahari|Lerik Rayonu|Masalli Rayonu|Mingacevir Sahari|Naftalan Sahari|Naxcivan Muxtar Respublikasi|Neftcala Rayonu|Oguz Rayonu|Qabala Rayonu|Qax Rayonu|Qazax Rayonu|Qobustan Rayonu|Quba Rayonu|Qubadli Rayonu|Qusar Rayonu|Saatli Rayonu|Sabirabad Rayonu|Saki Rayonu|Saki Sahari|Salyan Rayonu|Samaxi Rayonu|Samkir Rayonu|Samux Rayonu|Siyazan Rayonu|Sumqayit Sahari|Susa Rayonu|Susa Sahari|Tartar Rayonu|Tovuz Rayonu|Ucar Rayonu|Xacmaz Rayonu|Xankandi Sahari|Xanlar Rayonu|Xizi Rayonu|Xocali Rayonu|Xocavand Rayonu|Yardimli Rayonu|Yevlax Rayonu|Yevlax Sahari|Zangilan Rayonu|Zaqatala Rayonu|Zardab Rayonu" +) +s_a.append( + "Acklins and Crooked Islands|Bimini|Cat Island|Exuma|Freeport|Fresh Creek|Governor's Harbour|Green Turtle Cay|Harbour Island|High Rock|Inagua|Kemps Bay|Long Island|Marsh Harbour|Mayaguana|New Providence|Nicholls Town and Berry Islands|Ragged Island|Rock Sound|San Salvador and Rum Cay|Sandy Point" +) +s_a.append( + "Al Hadd|Al Manamah|Al Mintaqah al Gharbiyah|Al Mintaqah al Wusta|Al Mintaqah ash Shamaliyah|Al Muharraq|Ar Rifa' wa al Mintaqah al Janubiyah|Jidd Hafs|Juzur Hawar|Madinat 'Isa|Madinat Hamad|Sitrah" +) +s_a.append( + "Barguna|Barisal|Bhola|Jhalokati|Patuakhali|Pirojpur|Bandarban|Brahmanbaria|Chandpur|Chittagong|Comilla|Cox's Bazar|Feni|Khagrachari|Lakshmipur|Noakhali|Rangamati|Dhaka|Faridpur|Gazipur|Gopalganj|Jamalpur|Kishoreganj|Madaripur|Manikganj|Munshiganj|Mymensingh|Narayanganj|Narsingdi|Netrokona|Rajbari|Shariatpur|Sherpur|Tangail|Bagerhat|Chuadanga|Jessore|Jhenaidah|Khulna|Kushtia|Magura|Meherpur|Narail|Satkhira|Bogra|Dinajpur|Gaibandha|Jaipurhat|Kurigram|Lalmonirhat|Naogaon|Natore|Nawabganj|Nilphamari|Pabna|Panchagarh|Rajshahi|Rangpur|Sirajganj|Thakurgaon|Habiganj|Maulvi bazar|Sunamganj|Sylhet" +) +s_a.append( + "Bridgetown|Christ Church|Saint Andrew|Saint George|Saint James|Saint John|Saint Joseph|Saint Lucy|Saint Michael|Saint Peter|Saint Philip|Saint Thomas" +) +s_a.append( + "Brestskaya (Brest)|Homyel'skaya (Homyel')|Horad Minsk|Hrodzyenskaya (Hrodna)|Mahilyowskaya (Mahilyow)|Minskaya|Vitsyebskaya (Vitsyebsk)" +) +s_a.append( + "Antwerpen|Brabant Wallon|Brussels Capitol Region|Hainaut|Liege|Limburg|Luxembourg|Namur|Oost-Vlaanderen|Vlaams Brabant|West-Vlaanderen" +) +s_a.append("Belize|Cayo|Corozal|Orange Walk|Stann Creek|Toledo") +s_a.append( + "Alibori|Atakora|Atlantique|Borgou|Collines|Couffo|Donga|Littoral|Mono|Oueme|Plateau|Zou" +) +s_a.append( + "Devonshire|Hamilton|Hamilton|Paget|Pembroke|Saint George|Saint Georges|Sandys|Smiths|Southampton|Warwick" +) +s_a.append( + "Bumthang|Chhukha|Chirang|Daga|Geylegphug|Ha|Lhuntshi|Mongar|Paro|Pemagatsel|Punakha|Samchi|Samdrup Jongkhar|Shemgang|Tashigang|Thimphu|Tongsa|Wangdi Phodrang" +) +s_a.append("Beni|Chuquisaca|Cochabamba|La Paz|Oruro|Pando|Potosi|Santa Cruz|Tarija") +s_a.append("Federation of Bosnia and Herzegovina|Republika Srpska") +s_a.append( + "Central|Chobe|Francistown|Gaborone|Ghanzi|Kgalagadi|Kgatleng|Kweneng|Lobatse|Ngamiland|North-East|Selebi-Pikwe|South-East|Southern" +) +s_a.append( + "Acre|Alagoas|Amapa|Amazonas|Bahia|Ceara|Distrito Federal|Espirito Santo|Goias|Maranhao|Mato Grosso|Mato Grosso do Sul|Minas Gerais|Para|Paraiba|Parana|Pernambuco|Piaui|Rio de Janeiro|Rio Grande do Norte|Rio Grande do Sul|Rondonia|Roraima|Santa Catarina|Sao Paulo|Sergipe|Tocantins" +) +s_a.append("Anegada|Jost Van Dyke|Tortola|Virgin Gorda") +s_a.append("Belait|Brunei and Muara|Temburong|Tutong") +s_a.append( + "Blagoevgrad|Burgas|Dobrich|Gabrovo|Khaskovo|Kurdzhali|Kyustendil|Lovech|Montana|Pazardzhik|Pernik|Pleven|Plovdiv|Razgrad|Ruse|Shumen|Silistra|Sliven|Smolyan|Sofiya|Sofiya-Grad|Stara Zagora|Turgovishte|Varna|Veliko Turnovo|Vidin|Vratsa|Yambol" +) +s_a.append( + "Bale|Bam|Banwa|Bazega|Bougouriba|Boulgou|Boulkiemde|Comoe|Ganzourgou|Gnagna|Gourma|Houet|Ioba|Kadiogo|Kenedougou|Komandjari|Kompienga|Kossi|Koupelogo|Kouritenga|Kourweogo|Leraba|Loroum|Mouhoun|Nahouri|Namentenga|Naumbiel|Nayala|Oubritenga|Oudalan|Passore|Poni|Samentenga|Sanguie|Seno|Sissili|Soum|Sourou|Tapoa|Tuy|Yagha|Yatenga|Ziro|Zondomo|Zoundweogo" +) +s_a.append( + "Ayeyarwady|Bago|Chin State|Kachin State|Kayah State|Kayin State|Magway|Mandalay|Mon State|Rakhine State|Sagaing|Shan State|Tanintharyi|Yangon" +) +s_a.append( + "Bubanza|Bujumbura|Bururi|Cankuzo|Cibitoke|Gitega|Karuzi|Kayanza|Kirundo|Makamba|Muramvya|Muyinga|Mwaro|Ngozi|Rutana|Ruyigi" +) +s_a.append( + "Banteay Mean Cheay|Batdambang|Kampong Cham|Kampong Chhnang|Kampong Spoe|Kampong Thum|Kampot|Kandal|Kaoh Kong|Keb|Kracheh|Mondol Kiri|Otdar Mean Cheay|Pailin|Phnum Penh|Pouthisat|Preah Seihanu (Sihanoukville)|Preah Vihear|Prey Veng|Rotanah Kiri|Siem Reab|Stoeng Treng|Svay Rieng|Takev" +) +s_a.append( + "Adamaoua|Centre|Est|Extreme-Nord|Littoral|Nord|Nord-Ouest|Ouest|Sud|Sud-Ouest" +) +s_a.append( + "Alberta|British Columbia|Manitoba|New Brunswick|Newfoundland|Northwest Territories|Nova Scotia|Nunavut|Ontario|Prince Edward Island|Quebec|Saskatchewan|Yukon Territory" +) +s_a.append( + "Boa Vista|Brava|Maio|Mosteiros|Paul|Porto Novo|Praia|Ribeira Grande|Sal|Santa Catarina|Santa Cruz|Sao Domingos|Sao Filipe|Sao Nicolau|Sao Vicente|Tarrafal" +) +s_a.append("Creek|Eastern|Midland|South Town|Spot Bay|Stake Bay|West End|Western") +s_a.append( + "Bamingui-Bangoran|Bangui|Basse-Kotto|Gribingui|Haut-Mbomou|Haute-Kotto|Haute-Sangha|Kemo-Gribingui|Lobaye|Mbomou|Nana-Mambere|Ombella-Mpoko|Ouaka|Ouham|Ouham-Pende|Sangha|Vakaga" +) +s_a.append( + "Batha|Biltine|Borkou-Ennedi-Tibesti|Chari-Baguirmi|Guera|Kanem|Lac|Logone Occidental|Logone Oriental|Mayo-Kebbi|Moyen-Chari|Ouaddai|Salamat|Tandjile" +) +s_a.append( + "Aisen del General Carlos Ibanez del Campo|Antofagasta|Araucania|Atacama|Bio-Bio|Coquimbo|Libertador General Bernardo O'Higgins|Los Lagos|Magallanes y de la Antartica Chilena|Maule|Region Metropolitana (Santiago)|Tarapaca|Valparaiso" +) +s_a.append( + "Anhui|Beijing|Chongqing|Fujian|Gansu|Guangdong|Guangxi|Guizhou|Hainan|Hebei|Heilongjiang|Henan|Hubei|Hunan|Jiangsu|Jiangxi|Jilin|Liaoning|Nei Mongol|Ningxia|Qinghai|Shaanxi|Shandong|Shanghai|Shanxi|Sichuan|Tianjin|Xinjiang|Xizang (Tibet)|Yunnan|Zhejiang" +) +s_a.append("Christmas Island") +s_a.append("Clipperton Island") +s_a.append( + "Direction Island|Home Island|Horsburgh Island|North Keeling Island|South Island|West Island" +) +s_a.append( + "Amazonas|Antioquia|Arauca|Atlantico|Bolivar|Boyaca|Caldas|Caqueta|Casanare|Cauca|Cesar|Choco|Cordoba|Cundinamarca|Distrito Capital de Santa Fe de Bogota|Guainia|Guaviare|Huila|La Guajira|Magdalena|Meta|Narino|Norte de Santander|Putumayo|Quindio|Risaralda|San Andres y Providencia|Santander|Sucre|Tolima|Valle del Cauca|Vaupes|Vichada" +) +s_a.append( + "Anjouan (Nzwani)|Domoni|Fomboni|Grande Comore (Njazidja)|Moheli (Mwali)|Moroni|Moutsamoudou" +) +s_a.append( + "Bandundu|Bas-Congo|Equateur|Kasai-Occidental|Kasai-Oriental|Katanga|Kinshasa|Maniema|Nord-Kivu|Orientale|Sud-Kivu" +) +s_a.append( + "Bouenza|Brazzaville|Cuvette|Kouilou|Lekoumou|Likouala|Niari|Plateaux|Pool|Sangha" +) +s_a.append( + "Aitutaki|Atiu|Avarua|Mangaia|Manihiki|Manuae|Mauke|Mitiaro|Nassau Island|Palmerston|Penrhyn|Pukapuka|Rakahanga|Rarotonga|Suwarrow|Takutea" +) +s_a.append("Alajuela|Cartago|Guanacaste|Heredia|Limon|Puntarenas|San Jose") +s_a.append( + "Abengourou|Abidjan|Aboisso|Adiake'|Adzope|Agboville|Agnibilekrou|Ale'pe'|Bangolo|Beoumi|Biankouma|Bocanda|Bondoukou|Bongouanou|Bouafle|Bouake|Bouna|Boundiali|Dabakala|Dabon|Daloa|Danane|Daoukro|Dimbokro|Divo|Duekoue|Ferkessedougou|Gagnoa|Grand Bassam|Grand-Lahou|Guiglo|Issia|Jacqueville|Katiola|Korhogo|Lakota|Man|Mankono|Mbahiakro|Odienne|Oume|Sakassou|San-Pedro|Sassandra|Seguela|Sinfra|Soubre|Tabou|Tanda|Tiassale|Tiebissou|Tingrela|Touba|Toulepleu|Toumodi|Vavoua|Yamoussoukro|Zuenoula" +) +s_a.append( + "Bjelovarsko-Bilogorska Zupanija|Brodsko-Posavska Zupanija|Dubrovacko-Neretvanska Zupanija|Istarska Zupanija|Karlovacka Zupanija|Koprivnicko-Krizevacka Zupanija|Krapinsko-Zagorska Zupanija|Licko-Senjska Zupanija|Medimurska Zupanija|Osjecko-Baranjska Zupanija|Pozesko-Slavonska Zupanija|Primorsko-Goranska Zupanija|Sibensko-Kninska Zupanija|Sisacko-Moslavacka Zupanija|Splitsko-Dalmatinska Zupanija|Varazdinska Zupanija|Viroviticko-Podravska Zupanija|Vukovarsko-Srijemska Zupanija|Zadarska Zupanija|Zagreb|Zagrebacka Zupanija" +) +s_a.append( + "Camaguey|Ciego de Avila|Cienfuegos|Ciudad de La Habana|Granma|Guantanamo|Holguin|Isla de la Juventud|La Habana|Las Tunas|Matanzas|Pinar del Rio|Sancti Spiritus|Santiago de Cuba|Villa Clara" +) +s_a.append("Famagusta|Kyrenia|Larnaca|Limassol|Nicosia|Paphos") +s_a.append( + "Brnensky|Budejovicky|Jihlavsky|Karlovarsky|Kralovehradecky|Liberecky|Olomoucky|Ostravsky|Pardubicky|Plzensky|Praha|Stredocesky|Ustecky|Zlinsky" +) +s_a.append( + "Arhus|Bornholm|Fredericksberg|Frederiksborg|Fyn|Kobenhavn|Kobenhavns|Nordjylland|Ribe|Ringkobing|Roskilde|Sonderjylland|Storstrom|Vejle|Vestsjalland|Viborg" +) +s_a.append("'Ali Sabih|Dikhil|Djibouti|Obock|Tadjoura") +s_a.append( + "Saint Andrew|Saint David|Saint George|Saint John|Saint Joseph|Saint Luke|Saint Mark|Saint Patrick|Saint Paul|Saint Peter" +) +s_a.append( + "Azua|Baoruco|Barahona|Dajabon|Distrito Nacional|Duarte|El Seibo|Elias Pina|Espaillat|Hato Mayor|Independencia|La Altagracia|La Romana|La Vega|Maria Trinidad Sanchez|Monsenor Nouel|Monte Cristi|Monte Plata|Pedernales|Peravia|Puerto Plata|Salcedo|Samana|San Cristobal|San Juan|San Pedro de Macoris|Sanchez Ramirez|Santiago|Santiago Rodriguez|Valverde" +) +s_a.append( + "Azuay|Bolivar|Canar|Carchi|Chimborazo|Cotopaxi|El Oro|Esmeraldas|Galapagos|Guayas|Imbabura|Loja|Los Rios|Manabi|Morona-Santiago|Napo|Orellana|Pastaza|Pichincha|Sucumbios|Tungurahua|Zamora-Chinchipe" +) +s_a.append( + "Ad Daqahliyah|Al Bahr al Ahmar|Al Buhayrah|Al Fayyum|Al Gharbiyah|Al Iskandariyah|Al Isma'iliyah|Al Jizah|Al Minufiyah|Al Minya|Al Qahirah|Al Qalyubiyah|Al Wadi al Jadid|As Suways|Ash Sharqiyah|Aswan|Asyut|Bani Suwayf|Bur Sa'id|Dumyat|Janub Sina'|Kafr ash Shaykh|Matruh|Qina|Shamal Sina'|Suhaj" +) +s_a.append( + "Ahuachapan|Cabanas|Chalatenango|Cuscatlan|La Libertad|La Paz|La Union|Morazan|San Miguel|San Salvador|San Vicente|Santa Ana|Sonsonate|Usulutan" +) +s_a.append("Annobon|Bioko Norte|Bioko Sur|Centro Sur|Kie-Ntem|Litoral|Wele-Nzas") +s_a.append("Akale Guzay|Barka|Denkel|Hamasen|Sahil|Semhar|Senhit|Seraye") +s_a.append( + "Harjumaa (Tallinn)|Hiiumaa (Kardla)|Ida-Virumaa (Johvi)|Jarvamaa (Paide)|Jogevamaa (Jogeva)|Laane-Virumaa (Rakvere)|Laanemaa (Haapsalu)|Parnumaa (Parnu)|Polvamaa (Polva)|Raplamaa (Rapla)|Saaremaa (Kuessaare)|Tartumaa (Tartu)|Valgamaa (Valga)|Viljandimaa (Viljandi)|Vorumaa (Voru)" +) +s_a.append( + "Adis Abeba (Addis Ababa)|Afar|Amara|Dire Dawa|Gambela Hizboch|Hareri Hizb|Oromiya|Sumale|Tigray|YeDebub Biheroch Bihereseboch na Hizboch" +) +s_a.append("Europa Island") +s_a.append("Falkland Islands (Islas Malvinas)") +s_a.append("Bordoy|Eysturoy|Mykines|Sandoy|Skuvoy|Streymoy|Suduroy|Tvoroyri|Vagar") +s_a.append("Central|Eastern|Northern|Rotuma|Western") +s_a.append( + "Aland|Etela-Suomen Laani|Ita-Suomen Laani|Lansi-Suomen Laani|Lappi|Oulun Laani" +) +s_a.append( + "Alsace|Aquitaine|Auvergne|Basse-Normandie|Bourgogne|Bretagne|Centre|Champagne-Ardenne|Corse|Franche-Comte|Haute-Normandie|Ile-de-France|Languedoc-Roussillon|Limousin|Lorraine|Midi-Pyrenees|Nord-Pas-de-Calais|Pays de la Loire|Picardie|Poitou-Charentes|Provence-Alpes-Cote d'Azur|Rhone-Alpes" +) +s_a.append("French Guiana") +s_a.append( + "Archipel des Marquises|Archipel des Tuamotu|Archipel des Tubuai|Iles du Vent|Iles Sous-le-Vent" +) +s_a.append("Adelie Land|Ile Crozet|Iles Kerguelen|Iles Saint-Paul et Amsterdam") +s_a.append( + "Estuaire|Haut-Ogooue|Moyen-Ogooue|Ngounie|Nyanga|Ogooue-Ivindo|Ogooue-Lolo|Ogooue-Maritime|Woleu-Ntem" +) +s_a.append("Banjul|Central River|Lower River|North Bank|Upper River|Western") +s_a.append("Gaza Strip") +s_a.append( + "Abashis|Abkhazia or Ap'khazet'is Avtonomiuri Respublika (Sokhumi)|Adigenis|Ajaria or Acharis Avtonomiuri Respublika (Bat'umi)|Akhalgoris|Akhalk'alak'is|Akhalts'ikhis|Akhmetis|Ambrolauris|Aspindzis|Baghdat'is|Bolnisis|Borjomis|Ch'khorotsqus|Ch'okhatauris|Chiat'ura|Dedop'listsqaros|Dmanisis|Dushet'is|Gardabanis|Gori|Goris|Gurjaanis|Javis|K'arelis|K'ut'aisi|Kaspis|Kharagaulis|Khashuris|Khobis|Khonis|Lagodekhis|Lanch'khut'is|Lentekhis|Marneulis|Martvilis|Mestiis|Mts'khet'is|Ninotsmindis|Onis|Ozurget'is|P'ot'i|Qazbegis|Qvarlis|Rust'avi|Sach'kheris|Sagarejos|Samtrediis|Senakis|Sighnaghis|T'bilisi|T'elavis|T'erjolis|T'et'ritsqaros|T'ianet'is|Tqibuli|Ts'ageris|Tsalenjikhis|Tsalkis|Tsqaltubo|Vanis|Zestap'onis|Zugdidi|Zugdidis" +) +s_a.append( + "Baden-Wuerttemberg|Bayern|Berlin|Brandenburg|Bremen|Hamburg|Hessen|Mecklenburg-Vorpommern|Niedersachsen|Nordrhein-Westfalen|Rheinland-Pfalz|Saarland|Sachsen|Sachsen-Anhalt|Schleswig-Holstein|Thueringen" +) +s_a.append( + "Ashanti|Brong-Ahafo|Central|Eastern|Greater Accra|Northern|Upper East|Upper West|Volta|Western" +) +s_a.append("Gibraltar") +s_a.append("Ile du Lys|Ile Glorieuse") +s_a.append( + "Aitolia kai Akarnania|Akhaia|Argolis|Arkadhia|Arta|Attiki|Ayion Oros (Mt. Athos)|Dhodhekanisos|Drama|Evritania|Evros|Evvoia|Florina|Fokis|Fthiotis|Grevena|Ilia|Imathia|Ioannina|Irakleion|Kardhitsa|Kastoria|Kavala|Kefallinia|Kerkyra|Khalkidhiki|Khania|Khios|Kikladhes|Kilkis|Korinthia|Kozani|Lakonia|Larisa|Lasithi|Lesvos|Levkas|Magnisia|Messinia|Pella|Pieria|Preveza|Rethimni|Rodhopi|Samos|Serrai|Thesprotia|Thessaloniki|Trikala|Voiotia|Xanthi|Zakinthos" +) +s_a.append("Avannaa (Nordgronland)|Kitaa (Vestgronland)|Tunu (Ostgronland)") +s_a.append( + "Carriacou and Petit Martinique|Saint Andrew|Saint David|Saint George|Saint John|Saint Mark|Saint Patrick" +) +s_a.append( + "Basse-Terre|Grande-Terre|Iles de la Petite Terre|Iles des Saintes|Marie-Galante" +) +s_a.append("Guam") +s_a.append( + "Alta Verapaz|Baja Verapaz|Chimaltenango|Chiquimula|El Progreso|Escuintla|Guatemala|Huehuetenango|Izabal|Jalapa|Jutiapa|Peten|Quetzaltenango|Quiche|Retalhuleu|Sacatepequez|San Marcos|Santa Rosa|Solola|Suchitepequez|Totonicapan|Zacapa" +) +s_a.append( + "Castel|Forest|St. Andrew|St. Martin|St. Peter Port|St. Pierre du Bois|St. Sampson|St. Saviour|Torteval|Vale" +) +s_a.append( + "Beyla|Boffa|Boke|Conakry|Coyah|Dabola|Dalaba|Dinguiraye|Dubreka|Faranah|Forecariah|Fria|Gaoual|Gueckedou|Kankan|Kerouane|Kindia|Kissidougou|Koubia|Koundara|Kouroussa|Labe|Lelouma|Lola|Macenta|Mali|Mamou|Mandiana|Nzerekore|Pita|Siguiri|Telimele|Tougue|Yomou" +) +s_a.append("Bafata|Biombo|Bissau|Bolama-Bijagos|Cacheu|Gabu|Oio|Quinara|Tombali") +s_a.append( + "Barima-Waini|Cuyuni-Mazaruni|Demerara-Mahaica|East Berbice-Corentyne|Essequibo Islands-West Demerara|Mahaica-Berbice|Pomeroon-Supenaam|Potaro-Siparuni|Upper Demerara-Berbice|Upper Takutu-Upper Essequibo" +) +s_a.append("Artibonite|Centre|Grand'Anse|Nord|Nord-Est|Nord-Ouest|Ouest|Sud|Sud-Est") +s_a.append("Heard Island and McDonald Islands") +s_a.append("Holy See (Vatican City)") +s_a.append( + "Atlantida|Choluteca|Colon|Comayagua|Copan|Cortes|El Paraiso|Francisco Morazan|Gracias a Dios|Intibuca|Islas de la Bahia|La Paz|Lempira|Ocotepeque|Olancho|Santa Barbara|Valle|Yoro" +) +s_a.append("Hong Kong") +s_a.append("Howland Island") +s_a.append( + "Bacs-Kiskun|Baranya|Bekes|Bekescsaba|Borsod-Abauj-Zemplen|Budapest|Csongrad|Debrecen|Dunaujvaros|Eger|Fejer|Gyor|Gyor-Moson-Sopron|Hajdu-Bihar|Heves|Hodmezovasarhely|Jasz-Nagykun-Szolnok|Kaposvar|Kecskemet|Komarom-Esztergom|Miskolc|Nagykanizsa|Nograd|Nyiregyhaza|Pecs|Pest|Somogy|Sopron|Szabolcs-Szatmar-Bereg|Szeged|Szekesfehervar|Szolnok|Szombathely|Tatabanya|Tolna|Vas|Veszprem|Veszprem|Zala|Zalaegerszeg" +) +s_a.append( + "Akranes|Akureyri|Arnessysla|Austur-Bardhastrandarsysla|Austur-Hunavatnssysla|Austur-Skaftafellssysla|Borgarfjardharsysla|Dalasysla|Eyjafjardharsysla|Gullbringusysla|Hafnarfjordhur|Husavik|Isafjordhur|Keflavik|Kjosarsysla|Kopavogur|Myrasysla|Neskaupstadhur|Nordhur-Isafjardharsysla|Nordhur-Mulasys-la|Nordhur-Thingeyjarsysla|Olafsfjordhur|Rangarvallasysla|Reykjavik|Saudharkrokur|Seydhisfjordhur|Siglufjordhur|Skagafjardharsysla|Snaefellsnes-og Hnappadalssysla|Strandasysla|Sudhur-Mulasysla|Sudhur-Thingeyjarsysla|Vesttmannaeyjar|Vestur-Bardhastrandarsysla|Vestur-Hunavatnssysla|Vestur-Isafjardharsysla|Vestur-Skaftafellssysla" +) +s_a.append( + "Andaman and Nicobar Islands|Andhra Pradesh|Arunachal Pradesh|Assam|Bihar|Chandigarh|Chhattisgarh|Dadra and Nagar Haveli|Daman and Diu|Delhi|Goa|Gujarat|Haryana|Himachal Pradesh|Jammu and Kashmir|Jharkhand|Karnataka|Kerala|Lakshadweep|Madhya Pradesh|Maharashtra|Manipur|Meghalaya|Mizoram|Nagaland|Orissa|Pondicherry|Punjab|Rajasthan|Sikkim|Tamil Nadu|Telangana|Tripura|Uttar Pradesh|Uttaranchal|West Bengal" +) +s_a.append( + "Aceh|Bali|Banten|Bengkulu|East Timor|Gorontalo|Irian Jaya|Jakarta Raya|Jambi|Jawa Barat|Jawa Tengah|Jawa Timur|Kalimantan Barat|Kalimantan Selatan|Kalimantan Tengah|Kalimantan Timur|Kepulauan Bangka Belitung|Lampung|Maluku|Maluku Utara|Nusa Tenggara Barat|Nusa Tenggara Timur|Riau|Sulawesi Selatan|Sulawesi Tengah|Sulawesi Tenggara|Sulawesi Utara|Sumatera Barat|Sumatera Selatan|Sumatera Utara|Yogyakarta" +) +s_a.append( + "Ardabil|Azarbayjan-e Gharbi|Azarbayjan-e Sharqi|Bushehr|Chahar Mahall va Bakhtiari|Esfahan|Fars|Gilan|Golestan|Hamadan|Hormozgan|Ilam|Kerman|Kermanshah|Khorasan|Khuzestan|Kohgiluyeh va Buyer Ahmad|Kordestan|Lorestan|Markazi|Mazandaran|Qazvin|Qom|Semnan|Sistan va Baluchestan|Tehran|Yazd|Zanjan" +) +s_a.append( + "Al Anbar|Al Basrah|Al Muthanna|Al Qadisiyah|An Najaf|Arbil|As Sulaymaniyah|At Ta'mim|Babil|Baghdad|Dahuk|Dhi Qar|Diyala|Karbala'|Maysan|Ninawa|Salah ad Din|Wasit" +) +s_a.append( + "Carlow|Cavan|Clare|Cork|Donegal|Dublin|Galway|Kerry|Kildare|Kilkenny|Laois|Leitrim|Limerick|Longford|Louth|Mayo|Meath|Monaghan|Offaly|Roscommon|Sligo|Tipperary|Waterford|Westmeath|Wexford|Wicklow" +) +s_a.append( + "Antrim|Ards|Armagh|Ballymena|Ballymoney|Banbridge|Belfast|Carrickfergus|Castlereagh|Coleraine|Cookstown|Craigavon|Derry|Down|Dungannon|Fermanagh|Larne|Limavady|Lisburn|Magherafelt|Moyle|Newry and Mourne|Newtownabbey|North Down|Omagh|Strabane" +) +s_a.append("Central|Haifa|Jerusalem|Northern|Southern|Tel Aviv") +s_a.append( + "Abruzzo|Basilicata|Calabria|Campania|Emilia-Romagna|Friuli-Venezia Giulia|Lazio|Liguria|Lombardia|Marche|Molise|Piemonte|Puglia|Sardegna|Sicilia|Toscana|Trentino-Alto Adige|Umbria|Valle d'Aosta|Veneto" +) +s_a.append( + "Clarendon|Hanover|Kingston|Manchester|Portland|Saint Andrew|Saint Ann|Saint Catherine|Saint Elizabeth|Saint James|Saint Mary|Saint Thomas|Trelawny|Westmoreland" +) +s_a.append("Jan Mayen") +s_a.append( + "Aichi|Akita|Aomori|Chiba|Ehime|Fukui|Fukuoka|Fukushima|Gifu|Gumma|Hiroshima|Hokkaido|Hyogo|Ibaraki|Ishikawa|Iwate|Kagawa|Kagoshima|Kanagawa|Kochi|Kumamoto|Kyoto|Mie|Miyagi|Miyazaki|Nagano|Nagasaki|Nara|Niigata|Oita|Okayama|Okinawa|Osaka|Saga|Saitama|Shiga|Shimane|Shizuoka|Tochigi|Tokushima|Tokyo|Tottori|Toyama|Wakayama|Yamagata|Yamaguchi|Yamanashi" +) +s_a.append("Jarvis Island") +s_a.append("Jersey") +s_a.append("Johnston Atoll") +s_a.append( + "'Amman|Ajlun|Al 'Aqabah|Al Balqa'|Al Karak|Al Mafraq|At Tafilah|Az Zarqa'|Irbid|Jarash|Ma'an|Madaba" +) +s_a.append("Juan de Nova Island") +s_a.append( + "Almaty|Aqmola|Aqtobe|Astana|Atyrau|Batys Qazaqstan|Bayqongyr|Mangghystau|Ongtustik Qazaqstan|Pavlodar|Qaraghandy|Qostanay|Qyzylorda|Shyghys Qazaqstan|Soltustik Qazaqstan|Zhambyl" +) +s_a.append( + "Central|Coast|Eastern|Nairobi Area|North Eastern|Nyanza|Rift Valley|Western" +) +s_a.append( + "Abaiang|Abemama|Aranuka|Arorae|Banaba|Banaba|Beru|Butaritari|Central Gilberts|Gilbert Islands|Kanton|Kiritimati|Kuria|Line Islands|Line Islands|Maiana|Makin|Marakei|Nikunau|Nonouti|Northern Gilberts|Onotoa|Phoenix Islands|Southern Gilberts|Tabiteuea|Tabuaeran|Tamana|Tarawa|Tarawa|Teraina" +) +s_a.append( + "Chagang-do (Chagang Province)|Hamgyong-bukto (North Hamgyong Province)|Hamgyong-namdo (South Hamgyong Province)|Hwanghae-bukto (North Hwanghae Province)|Hwanghae-namdo (South Hwanghae Province)|Kaesong-si (Kaesong City)|Kangwon-do (Kangwon Province)|Namp'o-si (Namp'o City)|P'yongan-bukto (North P'yongan Province)|P'yongan-namdo (South P'yongan Province)|P'yongyang-si (P'yongyang City)|Yanggang-do (Yanggang Province)" +) +s_a.append( + "Ch'ungch'ong-bukto|Ch'ungch'ong-namdo|Cheju-do|Cholla-bukto|Cholla-namdo|Inch'on-gwangyoksi|Kangwon-do|Kwangju-gwangyoksi|Kyonggi-do|Kyongsang-bukto|Kyongsang-namdo|Pusan-gwangyoksi|Soul-t'ukpyolsi|Taegu-gwangyoksi|Taejon-gwangyoksi|Ulsan-gwangyoksi" +) +s_a.append("Al 'Asimah|Al Ahmadi|Al Farwaniyah|Al Jahra'|Hawalli") +s_a.append( + "Batken Oblasty|Bishkek Shaary|Chuy Oblasty (Bishkek)|Jalal-Abad Oblasty|Naryn Oblasty|Osh Oblasty|Talas Oblasty|Ysyk-Kol Oblasty (Karakol)" +) +s_a.append( + "Attapu|Bokeo|Bolikhamxai|Champasak|Houaphan|Khammouan|Louangnamtha|Louangphabang|Oudomxai|Phongsali|Salavan|Savannakhet|Viangchan|Viangchan|Xaignabouli|Xaisomboun|Xekong|Xiangkhoang" +) +s_a.append( + "Aizkraukles Rajons|Aluksnes Rajons|Balvu Rajons|Bauskas Rajons|Cesu Rajons|Daugavpils|Daugavpils Rajons|Dobeles Rajons|Gulbenes Rajons|Jekabpils Rajons|Jelgava|Jelgavas Rajons|Jurmala|Kraslavas Rajons|Kuldigas Rajons|Leipaja|Liepajas Rajons|Limbazu Rajons|Ludzas Rajons|Madonas Rajons|Ogres Rajons|Preilu Rajons|Rezekne|Rezeknes Rajons|Riga|Rigas Rajons|Saldus Rajons|Talsu Rajons|Tukuma Rajons|Valkas Rajons|Valmieras Rajons|Ventspils|Ventspils Rajons" +) +s_a.append("Beyrouth|Ech Chimal|Ej Jnoub|El Bekaa|Jabal Loubnane") +s_a.append( + "Berea|Butha-Buthe|Leribe|Mafeteng|Maseru|Mohales Hoek|Mokhotlong|Qacha's Nek|Quthing|Thaba-Tseka" +) +s_a.append( + "Bomi|Bong|Grand Bassa|Grand Cape Mount|Grand Gedeh|Grand Kru|Lofa|Margibi|Maryland|Montserrado|Nimba|River Cess|Sinoe" +) +s_a.append( + "Ajdabiya|Al 'Aziziyah|Al Fatih|Al Jabal al Akhdar|Al Jufrah|Al Khums|Al Kufrah|An Nuqat al Khams|Ash Shati'|Awbari|Az Zawiyah|Banghazi|Darnah|Ghadamis|Gharyan|Misratah|Murzuq|Sabha|Sawfajjin|Surt|Tarabulus|Tarhunah|Tubruq|Yafran|Zlitan" +) +s_a.append( + "Balzers|Eschen|Gamprin|Mauren|Planken|Ruggell|Schaan|Schellenberg|Triesen|Triesenberg|Vaduz" +) +s_a.append( + "Akmenes Rajonas|Alytaus Rajonas|Alytus|Anyksciu Rajonas|Birstonas|Birzu Rajonas|Druskininkai|Ignalinos Rajonas|Jonavos Rajonas|Joniskio Rajonas|Jurbarko Rajonas|Kaisiadoriu Rajonas|Kaunas|Kauno Rajonas|Kedainiu Rajonas|Kelmes Rajonas|Klaipeda|Klaipedos Rajonas|Kretingos Rajonas|Kupiskio Rajonas|Lazdiju Rajonas|Marijampole|Marijampoles Rajonas|Mazeikiu Rajonas|Moletu Rajonas|Neringa Pakruojo Rajonas|Palanga|Panevezio Rajonas|Panevezys|Pasvalio Rajonas|Plunges Rajonas|Prienu Rajonas|Radviliskio Rajonas|Raseiniu Rajonas|Rokiskio Rajonas|Sakiu Rajonas|Salcininku Rajonas|Siauliai|Siauliu Rajonas|Silales Rajonas|Silutes Rajonas|Sirvintu Rajonas|Skuodo Rajonas|Svencioniu Rajonas|Taurages Rajonas|Telsiu Rajonas|Traku Rajonas|Ukmerges Rajonas|Utenos Rajonas|Varenos Rajonas|Vilkaviskio Rajonas|Vilniaus Rajonas|Vilnius|Zarasu Rajonas" +) +s_a.append("Diekirch|Grevenmacher|Luxembourg") +s_a.append("Macau") +s_a.append( + "Aracinovo|Bac|Belcista|Berovo|Bistrica|Bitola|Blatec|Bogdanci|Bogomila|Bogovinje|Bosilovo|Brvenica|Cair (Skopje)|Capari|Caska|Cegrane|Centar (Skopje)|Centar Zupa|Cesinovo|Cucer-Sandevo|Debar|Delcevo|Delogozdi|Demir Hisar|Demir Kapija|Dobrusevo|Dolna Banjica|Dolneni|Dorce Petrov (Skopje)|Drugovo|Dzepciste|Gazi Baba (Skopje)|Gevgelija|Gostivar|Gradsko|Ilinden|Izvor|Jegunovce|Kamenjane|Karbinci|Karpos (Skopje)|Kavadarci|Kicevo|Kisela Voda (Skopje)|Klecevce|Kocani|Konce|Kondovo|Konopiste|Kosel|Kratovo|Kriva Palanka|Krivogastani|Krusevo|Kuklis|Kukurecani|Kumanovo|Labunista|Lipkovo|Lozovo|Lukovo|Makedonska Kamenica|Makedonski Brod|Mavrovi Anovi|Meseista|Miravci|Mogila|Murtino|Negotino|Negotino-Poloska|Novaci|Novo Selo|Oblesevo|Ohrid|Orasac|Orizari|Oslomej|Pehcevo|Petrovec|Plasnia|Podares|Prilep|Probistip|Radovis|Rankovce|Resen|Rosoman|Rostusa|Samokov|Saraj|Sipkovica|Sopiste|Sopotnika|Srbinovo|Star Dojran|Staravina|Staro Nagoricane|Stip|Struga|Strumica|Studenicani|Suto Orizari (Skopje)|Sveti Nikole|Tearce|Tetovo|Topolcani|Valandovo|Vasilevo|Veles|Velesta|Vevcani|Vinica|Vitoliste|Vranestica|Vrapciste|Vratnica|Vrutok|Zajas|Zelenikovo|Zileno|Zitose|Zletovo|Zrnovci" +) +s_a.append("Antananarivo|Antsiranana|Fianarantsoa|Mahajanga|Toamasina|Toliara") +s_a.append( + "Balaka|Blantyre|Chikwawa|Chiradzulu|Chitipa|Dedza|Dowa|Karonga|Kasungu|Likoma|Lilongwe|Machinga (Kasupe)|Mangochi|Mchinji|Mulanje|Mwanza|Mzimba|Nkhata Bay|Nkhotakota|Nsanje|Ntcheu|Ntchisi|Phalombe|Rumphi|Salima|Thyolo|Zomba" +) +s_a.append( + "Johor|Kedah|Kelantan|Labuan|Melaka|Negeri Sembilan|Pahang|Perak|Perlis|Pulau Pinang|Sabah|Sarawak|Selangor|Terengganu|Wilayah Persekutuan" +) +s_a.append( + "Alifu|Baa|Dhaalu|Faafu|Gaafu Alifu|Gaafu Dhaalu|Gnaviyani|Haa Alifu|Haa Dhaalu|Kaafu|Laamu|Lhaviyani|Maale|Meemu|Noonu|Raa|Seenu|Shaviyani|Thaa|Vaavu" +) +s_a.append("Gao|Kayes|Kidal|Koulikoro|Mopti|Segou|Sikasso|Tombouctou") +s_a.append("Valletta") +s_a.append("Man, Isle of") +s_a.append( + "Ailinginae|Ailinglaplap|Ailuk|Arno|Aur|Bikar|Bikini|Bokak|Ebon|Enewetak|Erikub|Jabat|Jaluit|Jemo|Kili|Kwajalein|Lae|Lib|Likiep|Majuro|Maloelap|Mejit|Mili|Namorik|Namu|Rongelap|Rongrik|Toke|Ujae|Ujelang|Utirik|Wotho|Wotje" +) +s_a.append("Martinique") +s_a.append( + "Adrar|Assaba|Brakna|Dakhlet Nouadhibou|Gorgol|Guidimaka|Hodh Ech Chargui|Hodh El Gharbi|Inchiri|Nouakchott|Tagant|Tiris Zemmour|Trarza" +) +s_a.append( + "Agalega Islands|Black River|Cargados Carajos Shoals|Flacq|Grand Port|Moka|Pamplemousses|Plaines Wilhems|Port Louis|Riviere du Rempart|Rodrigues|Savanne" +) +s_a.append("Mayotte") +s_a.append( + "Aguascalientes|Baja California|Baja California Sur|Campeche|Chiapas|Chihuahua|Coahuila de Zaragoza|Colima|Distrito Federal|Durango|Guanajuato|Guerrero|Hidalgo|Jalisco|Mexico|Michoacan de Ocampo|Morelos|Nayarit|Nuevo Leon|Oaxaca|Puebla|Queretaro de Arteaga|Quintana Roo|San Luis Potosi|Sinaloa|Sonora|Tabasco|Tamaulipas|Tlaxcala|Veracruz-Llave|Yucatan|Zacatecas" +) +s_a.append("Chuuk (Truk)|Kosrae|Pohnpei|Yap") +s_a.append("Midway Islands") +s_a.append( + "Balti|Cahul|Chisinau|Chisinau|Dubasari|Edinet|Gagauzia|Lapusna|Orhei|Soroca|Tighina|Ungheni" +) +s_a.append("Fontvieille|La Condamine|Monaco-Ville|Monte-Carlo") +s_a.append( + "Arhangay|Bayan-Olgiy|Bayanhongor|Bulgan|Darhan|Dornod|Dornogovi|Dundgovi|Dzavhan|Erdenet|Govi-Altay|Hentiy|Hovd|Hovsgol|Omnogovi|Ovorhangay|Selenge|Suhbaatar|Tov|Ulaanbaatar|Uvs" +) +s_a.append("Saint Anthony|Saint Georges|Saint Peter's") +s_a.append( + "Agadir|Al Hoceima|Azilal|Ben Slimane|Beni Mellal|Boulemane|Casablanca|Chaouen|El Jadida|El Kelaa des Srarhna|Er Rachidia|Essaouira|Fes|Figuig|Guelmim|Ifrane|Kenitra|Khemisset|Khenifra|Khouribga|Laayoune|Larache|Marrakech|Meknes|Nador|Ouarzazate|Oujda|Rabat-Sale|Safi|Settat|Sidi Kacem|Tan-Tan|Tanger|Taounate|Taroudannt|Tata|Taza|Tetouan|Tiznit" +) +s_a.append( + "Cabo Delgado|Gaza|Inhambane|Manica|Maputo|Nampula|Niassa|Sofala|Tete|Zambezia" +) +s_a.append( + "Caprivi|Erongo|Hardap|Karas|Khomas|Kunene|Ohangwena|Okavango|Omaheke|Omusati|Oshana|Oshikoto|Otjozondjupa" +) +s_a.append( + "Aiwo|Anabar|Anetan|Anibare|Baiti|Boe|Buada|Denigomodu|Ewa|Ijuw|Meneng|Nibok|Uaboe|Yaren" +) +s_a.append( + "Bagmati|Bheri|Dhawalagiri|Gandaki|Janakpur|Karnali|Kosi|Lumbini|Mahakali|Mechi|Narayani|Rapti|Sagarmatha|Seti" +) +s_a.append( + "Drenthe|Flevoland|Friesland|Gelderland|Groningen|Limburg|Noord-Brabant|Noord-Holland|Overijssel|Utrecht|Zeeland|Zuid-Holland" +) +s_a.append("Netherlands Antilles") +s_a.append("Iles Loyaute|Nord|Sud") +s_a.append( + "Akaroa|Amuri|Ashburton|Bay of Islands|Bruce|Buller|Chatham Islands|Cheviot|Clifton|Clutha|Cook|Dannevirke|Egmont|Eketahuna|Ellesmere|Eltham|Eyre|Featherston|Franklin|Golden Bay|Great Barrier Island|Grey|Hauraki Plains|Hawera|Hawke's Bay|Heathcote|Hikurangi|Hobson|Hokianga|Horowhenua|Hurunui|Hutt|Inangahua|Inglewood|Kaikoura|Kairanga|Kiwitea|Lake|Mackenzie|Malvern|Manaia|Manawatu|Mangonui|Maniototo|Marlborough|Masterton|Matamata|Mount Herbert|Ohinemuri|Opotiki|Oroua|Otamatea|Otorohanga|Oxford|Pahiatua|Paparua|Patea|Piako|Pohangina|Raglan|Rangiora|Rangitikei|Rodney|Rotorua|Runanga|Saint Kilda|Silverpeaks|Southland|Stewart Island|Stratford|Strathallan|Taranaki|Taumarunui|Taupo|Tauranga|Thames-Coromandel|Tuapeka|Vincent|Waiapu|Waiheke|Waihemo|Waikato|Waikohu|Waimairi|Waimarino|Waimate|Waimate West|Waimea|Waipa|Waipawa|Waipukurau|Wairarapa South|Wairewa|Wairoa|Waitaki|Waitomo|Waitotara|Wallace|Wanganui|Waverley|Westland|Whakatane|Whangarei|Whangaroa|Woodville" +) +s_a.append( + "Atlantico Norte|Atlantico Sur|Boaco|Carazo|Chinandega|Chontales|Esteli|Granada|Jinotega|Leon|Madriz|Managua|Masaya|Matagalpa|Nueva Segovia|Rio San Juan|Rivas" +) +s_a.append("Agadez|Diffa|Dosso|Maradi|Niamey|Tahoua|Tillaberi|Zinder") +s_a.append( + "Abia|Abuja Federal Capital Territory|Adamawa|Akwa Ibom|Anambra|Bauchi|Bayelsa|Benue|Borno|Cross River|Delta|Ebonyi|Edo|Ekiti|Enugu|Gombe|Imo|Jigawa|Kaduna|Kano|Katsina|Kebbi|Kogi|Kwara|Lagos|Nassarawa|Niger|Ogun|Ondo|Osun|Oyo|Plateau|Rivers|Sokoto|Taraba|Yobe|Zamfara" +) +s_a.append("Niue") +s_a.append("Norfolk Island") +s_a.append("Northern Islands|Rota|Saipan|Tinian") +s_a.append( + "Akershus|Aust-Agder|Buskerud|Finnmark|Hedmark|Hordaland|More og Romsdal|Nord-Trondelag|Nordland|Oppland|Oslo|Ostfold|Rogaland|Sogn og Fjordane|Sor-Trondelag|Telemark|Troms|Vest-Agder|Vestfold" +) +s_a.append( + "Ad Dakhiliyah|Al Batinah|Al Wusta|Ash Sharqiyah|Az Zahirah|Masqat|Musandam|Zufar" +) +s_a.append( + "Balochistan|Federally Administered Tribal Areas|Islamabad Capital Territory|North-West Frontier Province|Punjab|Sindh" +) +s_a.append( + "Aimeliik|Airai|Angaur|Hatobohei|Kayangel|Koror|Melekeok|Ngaraard|Ngarchelong|Ngardmau|Ngatpang|Ngchesar|Ngeremlengui|Ngiwal|Palau Island|Peleliu|Sonsoral|Tobi" +) +s_a.append( + "Bocas del Toro|Chiriqui|Cocle|Colon|Darien|Herrera|Los Santos|Panama|San Blas|Veraguas" +) +s_a.append( + "Bougainville|Central|Chimbu|East New Britain|East Sepik|Eastern Highlands|Enga|Gulf|Madang|Manus|Milne Bay|Morobe|National Capital|New Ireland|Northern|Sandaun|Southern Highlands|West New Britain|Western|Western Highlands" +) +s_a.append( + "Alto Paraguay|Alto Parana|Amambay|Asuncion (city)|Boqueron|Caaguazu|Caazapa|Canindeyu|Central|Concepcion|Cordillera|Guaira|Itapua|Misiones|Neembucu|Paraguari|Presidente Hayes|San Pedro" +) +s_a.append( + "Amazonas|Ancash|Apurimac|Arequipa|Ayacucho|Cajamarca|Callao|Cusco|Huancavelica|Huanuco|Ica|Junin|La Libertad|Lambayeque|Lima|Loreto|Madre de Dios|Moquegua|Pasco|Piura|Puno|San Martin|Tacna|Tumbes|Ucayali" +) +s_a.append( + "Abra|Agusan del Norte|Agusan del Sur|Aklan|Albay|Angeles|Antique|Aurora|Bacolod|Bago|Baguio|Bais|Basilan|Basilan City|Bataan|Batanes|Batangas|Batangas City|Benguet|Bohol|Bukidnon|Bulacan|Butuan|Cabanatuan|Cadiz|Cagayan|Cagayan de Oro|Calbayog|Caloocan|Camarines Norte|Camarines Sur|Camiguin|Canlaon|Capiz|Catanduanes|Cavite|Cavite City|Cebu|Cebu City|Cotabato|Dagupan|Danao|Dapitan|Davao City Davao|Davao del Sur|Davao Oriental|Dipolog|Dumaguete|Eastern Samar|General Santos|Gingoog|Ifugao|Iligan|Ilocos Norte|Ilocos Sur|Iloilo|Iloilo City|Iriga|Isabela|Kalinga-Apayao|La Carlota|La Union|Laguna|Lanao del Norte|Lanao del Sur|Laoag|Lapu-Lapu|Legaspi|Leyte|Lipa|Lucena|Maguindanao|Mandaue|Manila|Marawi|Marinduque|Masbate|Mindoro Occidental|Mindoro Oriental|Misamis Occidental|Misamis Oriental|Mountain|Naga|Negros Occidental|Negros Oriental|North Cotabato|Northern Samar|Nueva Ecija|Nueva Vizcaya|Olongapo|Ormoc|Oroquieta|Ozamis|Pagadian|Palawan|Palayan|Pampanga|Pangasinan|Pasay|Puerto Princesa|Quezon|Quezon City|Quirino|Rizal|Romblon|Roxas|Samar|San Carlos (in Negros Occidental)|San Carlos (in Pangasinan)|San Jose|San Pablo|Silay|Siquijor|Sorsogon|South Cotabato|Southern Leyte|Sultan Kudarat|Sulu|Surigao|Surigao del Norte|Surigao del Sur|Tacloban|Tagaytay|Tagbilaran|Tangub|Tarlac|Tawitawi|Toledo|Trece Martires|Zambales|Zamboanga|Zamboanga del Norte|Zamboanga del Sur" +) +s_a.append("Pitcaim Islands") +s_a.append( + "Dolnoslaskie|Kujawsko-Pomorskie|Lodzkie|Lubelskie|Lubuskie|Malopolskie|Mazowieckie|Opolskie|Podkarpackie|Podlaskie|Pomorskie|Slaskie|Swietokrzyskie|Warminsko-Mazurskie|Wielkopolskie|Zachodniopomorskie" +) +s_a.append( + "Acores (Azores)|Aveiro|Beja|Braga|Braganca|Castelo Branco|Coimbra|Evora|Faro|Guarda|Leiria|Lisboa|Madeira|Portalegre|Porto|Santarem|Setubal|Viana do Castelo|Vila Real|Viseu" +) +s_a.append( + "Adjuntas|Aguada|Aguadilla|Aguas Buenas|Aibonito|Anasco|Arecibo|Arroyo|Barceloneta|Barranquitas|Bayamon|Cabo Rojo|Caguas|Camuy|Canovanas|Carolina|Catano|Cayey|Ceiba|Ciales|Cidra|Coamo|Comerio|Corozal|Culebra|Dorado|Fajardo|Florida|Guanica|Guayama|Guayanilla|Guaynabo|Gurabo|Hatillo|Hormigueros|Humacao|Isabela|Jayuya|Juana Diaz|Juncos|Lajas|Lares|Las Marias|Las Piedras|Loiza|Luquillo|Manati|Maricao|Maunabo|Mayaguez|Moca|Morovis|Naguabo|Naranjito|Orocovis|Patillas|Penuelas|Ponce|Quebradillas|Rincon|Rio Grande|Sabana Grande|Salinas|San German|San Juan|San Lorenzo|San Sebastian|Santa Isabel|Toa Alta|Toa Baja|Trujillo Alto|Utuado|Vega Alta|Vega Baja|Vieques|Villalba|Yabucoa|Yauco" +) +s_a.append( + "Ad Dawhah|Al Ghuwayriyah|Al Jumayliyah|Al Khawr|Al Wakrah|Ar Rayyan|Jarayan al Batinah|Madinat ash Shamal|Umm Salal" +) +s_a.append("Reunion") +s_a.append( + "Alba|Arad|Arges|Bacau|Bihor|Bistrita-Nasaud|Botosani|Braila|Brasov|Bucuresti|Buzau|Calarasi|Caras-Severin|Cluj|Constanta|Covasna|Dimbovita|Dolj|Galati|Giurgiu|Gorj|Harghita|Hunedoara|Ialomita|Iasi|Maramures|Mehedinti|Mures|Neamt|Olt|Prahova|Salaj|Satu Mare|Sibiu|Suceava|Teleorman|Timis|Tulcea|Vaslui|Vilcea|Vrancea" +) +s_a.append( + "Adygeya (Maykop)|Aginskiy Buryatskiy (Aginskoye)|Altay (Gorno-Altaysk)|Altayskiy (Barnaul)|Amurskaya (Blagoveshchensk)|Arkhangel'skaya|Astrakhanskaya|Bashkortostan (Ufa)|Belgorodskaya|Bryanskaya|Buryatiya (Ulan-Ude)|Chechnya (Groznyy)|Chelyabinskaya|Chitinskaya|Chukotskiy (Anadyr')|Chuvashiya (Cheboksary)|Dagestan (Makhachkala)|Evenkiyskiy (Tura)|Ingushetiya (Nazran')|Irkutskaya|Ivanovskaya|Kabardino-Balkariya (Nal'chik)|Kaliningradskaya|Kalmykiya (Elista)|Kaluzhskaya|Kamchatskaya (Petropavlovsk-Kamchatskiy)|Karachayevo-Cherkesiya (Cherkessk)|Kareliya (Petrozavodsk)|Kemerovskaya|Khabarovskiy|Khakasiya (Abakan)|Khanty-Mansiyskiy (Khanty-Mansiysk)|Kirovskaya|Komi (Syktyvkar)|Komi-Permyatskiy (Kudymkar)|Koryakskiy (Palana)|Kostromskaya|Krasnodarskiy|Krasnoyarskiy|Kurganskaya|Kurskaya|Leningradskaya|Lipetskaya|Magadanskaya|Mariy-El (Yoshkar-Ola)|Mordoviya (Saransk)|Moskovskaya|Moskva (Moscow)|Murmanskaya|Nenetskiy (Nar'yan-Mar)|Nizhegorodskaya|Novgorodskaya|Novosibirskaya|Omskaya|Orenburgskaya|Orlovskaya (Orel)|Penzenskaya|Permskaya|Primorskiy (Vladivostok)|Pskovskaya|Rostovskaya|Ryazanskaya|Sakha (Yakutsk)|Sakhalinskaya (Yuzhno-Sakhalinsk)|Samarskaya|Sankt-Peterburg (Saint Petersburg)|Saratovskaya|Severnaya Osetiya-Alaniya [North Ossetia] (Vladikavkaz)|Smolenskaya|Stavropol'skiy|Sverdlovskaya (Yekaterinburg)|Tambovskaya|Tatarstan (Kazan')|Taymyrskiy (Dudinka)|Tomskaya|Tul'skaya|Tverskaya|Tyumenskaya|Tyva (Kyzyl)|Udmurtiya (Izhevsk)|Ul'yanovskaya|Ust'-Ordynskiy Buryatskiy (Ust'-Ordynskiy)|Vladimirskaya|Volgogradskaya|Vologodskaya|Voronezhskaya|Yamalo-Nenetskiy (Salekhard)|Yaroslavskaya|Yevreyskaya" +) +s_a.append( + "Butare|Byumba|Cyangugu|Gikongoro|Gisenyi|Gitarama|Kibungo|Kibuye|Kigali Rurale|Kigali-ville|Ruhengeri|Umutara" +) +s_a.append("Ascension|Saint Helena|Tristan da Cunha") +s_a.append( + "Christ Church Nichola Town|Saint Anne Sandy Point|Saint George Basseterre|Saint George Gingerland|Saint James Windward|Saint John Capisterre|Saint John Figtree|Saint Mary Cayon|Saint Paul Capisterre|Saint Paul Charlestown|Saint Peter Basseterre|Saint Thomas Lowland|Saint Thomas Middle Island|Trinity Palmetto Point" +) +s_a.append( + "Anse-la-Raye|Castries|Choiseul|Dauphin|Dennery|Gros Islet|Laborie|Micoud|Praslin|Soufriere|Vieux Fort" +) +s_a.append("Miquelon|Saint Pierre") +s_a.append("Charlotte|Grenadines|Saint Andrew|Saint David|Saint George|Saint Patrick") +s_a.append( + "A'ana|Aiga-i-le-Tai|Atua|Fa'asaleleaga|Gaga'emauga|Gagaifomauga|Palauli|Satupa'itea|Tuamasaga|Va'a-o-Fonoti|Vaisigano" +) +s_a.append( + "Acquaviva|Borgo Maggiore|Chiesanuova|Domagnano|Faetano|Fiorentino|Monte Giardino|San Marino|Serravalle" +) +s_a.append("Principe|Sao Tome") +s_a.append( + "'Asir|Al Bahah|Al Hudud ash Shamaliyah|Al Jawf|Al Madinah|Al Qasim|Ar Riyad|Ash Sharqiyah (Eastern Province)|Ha'il|Jizan|Makkah|Najran|Tabuk" +) +s_a.append( + "Aberdeen City|Aberdeenshire|Angus|Argyll and Bute|City of Edinburgh|Clackmannanshire|Dumfries and Galloway|Dundee City|East Ayrshire|East Dunbartonshire|East Lothian|East Renfrewshire|Eilean Siar (Western Isles)|Falkirk|Fife|Glasgow City|Highland|Inverclyde|Midlothian|Moray|North Ayrshire|North Lanarkshire|Orkney Islands|Perth and Kinross|Renfrewshire|Shetland Islands|South Ayrshire|South Lanarkshire|Stirling|The Scottish Borders|West Dunbartonshire|West Lothian" +) +s_a.append( + "Dakar|Diourbel|Fatick|Kaolack|Kolda|Louga|Saint-Louis|Tambacounda|Thies|Ziguinchor" +) +s_a.append( + "Anse aux Pins|Anse Boileau|Anse Etoile|Anse Louis|Anse Royale|Baie Lazare|Baie Sainte Anne|Beau Vallon|Bel Air|Bel Ombre|Cascade|Glacis|Grand' Anse (on Mahe)|Grand' Anse (on Praslin)|La Digue|La Riviere Anglaise|Mont Buxton|Mont Fleuri|Plaisance|Pointe La Rue|Port Glaud|Saint Louis|Takamaka" +) +s_a.append("Eastern|Northern|Southern|Western") +s_a.append("Singapore") +s_a.append( + "Banskobystricky|Bratislavsky|Kosicky|Nitriansky|Presovsky|Trenciansky|Trnavsky|Zilinsky" +) +s_a.append( + "Ajdovscina|Beltinci|Bled|Bohinj|Borovnica|Bovec|Brda|Brezice|Brezovica|Cankova-Tisina|Celje|Cerklje na Gorenjskem|Cerknica|Cerkno|Crensovci|Crna na Koroskem|Crnomelj|Destrnik-Trnovska Vas|Divaca|Dobrepolje|Dobrova-Horjul-Polhov Gradec|Dol pri Ljubljani|Domzale|Dornava|Dravograd|Duplek|Gorenja Vas-Poljane|Gorisnica|Gornja Radgona|Gornji Grad|Gornji Petrovci|Grosuplje|Hodos Salovci|Hrastnik|Hrpelje-Kozina|Idrija|Ig|Ilirska Bistrica|Ivancna Gorica|Izola|Jesenice|Jursinci|Kamnik|Kanal|Kidricevo|Kobarid|Kobilje|Kocevje|Komen|Koper|Kozje|Kranj|Kranjska Gora|Krsko|Kungota|Kuzma|Lasko|Lenart|Lendava|Litija|Ljubljana|Ljubno|Ljutomer|Logatec|Loska Dolina|Loski Potok|Luce|Lukovica|Majsperk|Maribor|Medvode|Menges|Metlika|Mezica|Miren-Kostanjevica|Mislinja|Moravce|Moravske Toplice|Mozirje|Murska Sobota|Muta|Naklo|Nazarje|Nova Gorica|Novo Mesto|Odranci|Ormoz|Osilnica|Pesnica|Piran|Pivka|Podcetrtek|Podvelka-Ribnica|Postojna|Preddvor|Ptuj|Puconci|Race-Fram|Radece|Radenci|Radlje ob Dravi|Radovljica|Ravne-Prevalje|Ribnica|Rogasevci|Rogaska Slatina|Rogatec|Ruse|Semic|Sencur|Sentilj|Sentjernej|Sentjur pri Celju|Sevnica|Sezana|Skocjan|Skofja Loka|Skofljica|Slovenj Gradec|Slovenska Bistrica|Slovenske Konjice|Smarje pri Jelsah|Smartno ob Paki|Sostanj|Starse|Store|Sveti Jurij|Tolmin|Trbovlje|Trebnje|Trzic|Turnisce|Velenje|Velike Lasce|Videm|Vipava|Vitanje|Vodice|Vojnik|Vrhnika|Vuzenica|Zagorje ob Savi|Zalec|Zavrc|Zelezniki|Ziri|Zrece" +) +s_a.append( + "Bellona|Central|Choiseul (Lauru)|Guadalcanal|Honiara|Isabel|Makira|Malaita|Rennell|Temotu|Western" +) +s_a.append( + "Awdal|Bakool|Banaadir|Bari|Bay|Galguduud|Gedo|Hiiraan|Jubbada Dhexe|Jubbada Hoose|Mudug|Nugaal|Sanaag|Shabeellaha Dhexe|Shabeellaha Hoose|Sool|Togdheer|Woqooyi Galbeed" +) +s_a.append( + "Eastern Cape|Free State|Gauteng|KwaZulu-Natal|Mpumalanga|North-West|Northern Cape|Northern Province|Western Cape" +) +s_a.append( + "Bird Island|Bristol Island|Clerke Rocks|Montagu Island|Saunders Island|South Georgia|Southern Thule|Traversay Islands" +) +s_a.append( + "Andalucia|Aragon|Asturias|Baleares (Balearic Islands)|Canarias (Canary Islands)|Cantabria|Castilla y Leon|Castilla-La Mancha|Cataluna|Ceuta|Communidad Valencian|Extremadura|Galicia|Islas Chafarinas|La Rioja|Madrid|Melilla|Murcia|Navarra|Pais Vasco (Basque Country)|Penon de Alhucemas|Penon de Velez de la Gomera" +) +s_a.append("Spratly Islands") +s_a.append( + "Central|Eastern|North Central|North Eastern|North Western|Northern|Sabaragamuwa|Southern|Uva|Western" +) +s_a.append( + "A'ali an Nil|Al Bahr al Ahmar|Al Buhayrat|Al Jazirah|Al Khartum|Al Qadarif|Al Wahdah|An Nil al Abyad|An Nil al Azraq|Ash Shamaliyah|Bahr al Jabal|Gharb al Istiwa'iyah|Gharb Bahr al Ghazal|Gharb Darfur|Gharb Kurdufan|Janub Darfur|Janub Kurdufan|Junqali|Kassala|Nahr an Nil|Shamal Bahr al Ghazal|Shamal Darfur|Shamal Kurdufan|Sharq al Istiwa'iyah|Sinnar|Warab" +) +s_a.append( + "Brokopondo|Commewijne|Coronie|Marowijne|Nickerie|Para|Paramaribo|Saramacca|Sipaliwini|Wanica" +) +s_a.append( + "Barentsoya|Bjornoya|Edgeoya|Hopen|Kvitoya|Nordaustandet|Prins Karls Forland|Spitsbergen" +) +s_a.append("Hhohho|Lubombo|Manzini|Shiselweni") +s_a.append( + "Blekinge|Dalarnas|Gavleborgs|Gotlands|Hallands|Jamtlands|Jonkopings|Kalmar|Kronobergs|Norrbottens|Orebro|Ostergotlands|Skane|Sodermanlands|Stockholms|Uppsala|Varmlands|Vasterbottens|Vasternorrlands|Vastmanlands|Vastra Gotalands" +) +s_a.append( + "Aargau|Ausser-Rhoden|Basel-Landschaft|Basel-Stadt|Bern|Fribourg|Geneve|Glarus|Graubunden|Inner-Rhoden|Jura|Luzern|Neuchatel|Nidwalden|Obwalden|Sankt Gallen|Schaffhausen|Schwyz|Solothurn|Thurgau|Ticino|Uri|Valais|Vaud|Zug|Zurich" +) +s_a.append( + "Al Hasakah|Al Ladhiqiyah|Al Qunaytirah|Ar Raqqah|As Suwayda'|Dar'a|Dayr az Zawr|Dimashq|Halab|Hamah|Hims|Idlib|Rif Dimashq|Tartus" +) +s_a.append( + "Chang-hua|Chi-lung|Chia-i|Chia-i|Chung-hsing-hsin-ts'un|Hsin-chu|Hsin-chu|Hua-lien|I-lan|Kao-hsiung|Kao-hsiung|Miao-li|Nan-t'ou|P'eng-hu|P'ing-tung|T'ai-chung|T'ai-chung|T'ai-nan|T'ai-nan|T'ai-pei|T'ai-pei|T'ai-tung|T'ao-yuan|Yun-lin" +) +s_a.append("Viloyati Khatlon|Viloyati Leninobod|Viloyati Mukhtori Kuhistoni Badakhshon") +s_a.append( + "Arusha|Dar es Salaam|Dodoma|Iringa|Kagera|Kigoma|Kilimanjaro|Lindi|Mara|Mbeya|Morogoro|Mtwara|Mwanza|Pemba North|Pemba South|Pwani|Rukwa|Ruvuma|Shinyanga|Singida|Tabora|Tanga|Zanzibar Central/South|Zanzibar North|Zanzibar Urban/West" +) +s_a.append( + "Amnat Charoen|Ang Thong|Buriram|Chachoengsao|Chai Nat|Chaiyaphum|Chanthaburi|Chiang Mai|Chiang Rai|Chon Buri|Chumphon|Kalasin|Kamphaeng Phet|Kanchanaburi|Khon Kaen|Krabi|Krung Thep Mahanakhon (Bangkok)|Lampang|Lamphun|Loei|Lop Buri|Mae Hong Son|Maha Sarakham|Mukdahan|Nakhon Nayok|Nakhon Pathom|Nakhon Phanom|Nakhon Ratchasima|Nakhon Sawan|Nakhon Si Thammarat|Nan|Narathiwat|Nong Bua Lamphu|Nong Khai|Nonthaburi|Pathum Thani|Pattani|Phangnga|Phatthalung|Phayao|Phetchabun|Phetchaburi|Phichit|Phitsanulok|Phra Nakhon Si Ayutthaya|Phrae|Phuket|Prachin Buri|Prachuap Khiri Khan|Ranong|Ratchaburi|Rayong|Roi Et|Sa Kaeo|Sakon Nakhon|Samut Prakan|Samut Sakhon|Samut Songkhram|Sara Buri|Satun|Sing Buri|Sisaket|Songkhla|Sukhothai|Suphan Buri|Surat Thani|Surin|Tak|Trang|Trat|Ubon Ratchathani|Udon Thani|Uthai Thani|Uttaradit|Yala|Yasothon" +) +s_a.append("Tobago") +s_a.append("De La Kara|Des Plateaux|Des Savanes|Du Centre|Maritime") +s_a.append("Atafu|Fakaofo|Nukunonu") +s_a.append("Ha'apai|Tongatapu|Vava'u") +s_a.append( + "Arima|Caroni|Mayaro|Nariva|Port-of-Spain|Saint Andrew|Saint David|Saint George|Saint Patrick|San Fernando|Victoria" +) +s_a.append( + "Ariana|Beja|Ben Arous|Bizerte|El Kef|Gabes|Gafsa|Jendouba|Kairouan|Kasserine|Kebili|Mahdia|Medenine|Monastir|Nabeul|Sfax|Sidi Bou Zid|Siliana|Sousse|Tataouine|Tozeur|Tunis|Zaghouan" +) +s_a.append( + "Adana|Adiyaman|Afyon|Agri|Aksaray|Amasya|Ankara|Antalya|Ardahan|Artvin|Aydin|Balikesir|Bartin|Batman|Bayburt|Bilecik|Bingol|Bitlis|Bolu|Burdur|Bursa|Canakkale|Cankiri|Corum|Denizli|Diyarbakir|Duzce|Edirne|Elazig|Erzincan|Erzurum|Eskisehir|Gaziantep|Giresun|Gumushane|Hakkari|Hatay|Icel|Igdir|Isparta|Istanbul|Izmir|Kahramanmaras|Karabuk|Karaman|Kars|Kastamonu|Kayseri|Kilis|Kirikkale|Kirklareli|Kirsehir|Kocaeli|Konya|Kutahya|Malatya|Manisa|Mardin|Mugla|Mus|Nevsehir|Nigde|Ordu|Osmaniye|Rize|Sakarya|Samsun|Sanliurfa|Siirt|Sinop|Sirnak|Sivas|Tekirdag|Tokat|Trabzon|Tunceli|Usak|Van|Yalova|Yozgat|Zonguldak" +) +s_a.append( + "Ahal Welayaty|Balkan Welayaty|Dashhowuz Welayaty|Lebap Welayaty|Mary Welayaty" +) +s_a.append("Tuvalu") +s_a.append( + "Adjumani|Apac|Arua|Bugiri|Bundibugyo|Bushenyi|Busia|Gulu|Hoima|Iganga|Jinja|Kabale|Kabarole|Kalangala|Kampala|Kamuli|Kapchorwa|Kasese|Katakwi|Kibale|Kiboga|Kisoro|Kitgum|Kotido|Kumi|Lira|Luwero|Masaka|Masindi|Mbale|Mbarara|Moroto|Moyo|Mpigi|Mubende|Mukono|Nakasongola|Nebbi|Ntungamo|Pallisa|Rakai|Rukungiri|Sembabule|Soroti|Tororo" +) +s_a.append( + "Avtonomna Respublika Krym (Simferopol')|Cherkas'ka (Cherkasy)|Chernihivs'ka (Chernihiv)|Chernivets'ka (Chernivtsi)|Dnipropetrovs'ka (Dnipropetrovs'k)|Donets'ka (Donets'k)|Ivano-Frankivs'ka (Ivano-Frankivs'k)|Kharkivs'ka (Kharkiv)|Khersons'ka (Kherson)|Khmel'nyts'ka (Khmel'nyts'kyy)|Kirovohrads'ka (Kirovohrad)|Kyyiv|Kyyivs'ka (Kiev)|L'vivs'ka (L'viv)|Luhans'ka (Luhans'k)|Mykolayivs'ka (Mykolayiv)|Odes'ka (Odesa)|Poltavs'ka (Poltava)|Rivnens'ka (Rivne)|Sevastopol'|Sums'ka (Sumy)|Ternopil's'ka (Ternopil')|Vinnyts'ka (Vinnytsya)|Volyns'ka (Luts'k)|Zakarpats'ka (Uzhhorod)|Zaporiz'ka (Zaporizhzhya)|Zhytomyrs'ka (Zhytomyr)" +) +s_a.append( + "'Ajman|Abu Zaby (Abu Dhabi)|Al Fujayrah|Ash Shariqah (Sharjah)|Dubayy (Dubai)|Ra's al Khaymah|Umm al Qaywayn" +) +s_a.append( + "Barking and Dagenham|Barnet|Barnsley|Bath and North East Somerset|Bedfordshire|Bexley|Birmingham|Blackburn with Darwen|Blackpool|Bolton|Bournemouth|Bracknell Forest|Bradford|Brent|Brighton and Hove|Bromley|Buckinghamshire|Bury|Calderdale|Cambridgeshire|Camden|Cheshire|City of Bristol|City of Kingston upon Hull|City of London|Cornwall|Coventry|Croydon|Cumbria|Darlington|Derby|Derbyshire|Devon|Doncaster|Dorset|Dudley|Durham|Ealing|East Riding of Yorkshire|East Sussex|Enfield|Essex|Gateshead|Gloucestershire|Greenwich|Hackney|Halton|Hammersmith and Fulham|Hampshire|Haringey|Harrow|Hartlepool|Havering|Herefordshire|Hertfordshire|Hillingdon|Hounslow|Isle of Wight|Islington|Kensington and Chelsea|Kent|Kingston upon Thames|Kirklees|Knowsley|Lambeth|Lancashire|Leeds|Leicester|Leicestershire|Lewisham|Lincolnshire|Liverpool|Luton|Manchester|Medway|Merton|Middlesbrough|Milton Keynes|Newcastle upon Tyne|Newham|Norfolk|North East Lincolnshire|North Lincolnshire|North Somerset|North Tyneside|North Yorkshire|Northamptonshire|Northumberland|Nottingham|Nottinghamshire|Oldham|Oxfordshire|Peterborough|Plymouth|Poole|Portsmouth|Reading|Redbridge|Redcar and Cleveland|Richmond upon Thames|Rochdale|Rotherham|Rutland|Salford|Sandwell|Sefton|Sheffield|Shropshire|Slough|Solihull|Somerset|South Gloucestershire|South Tyneside|Southampton|Southend-on-Sea|Southwark|St. Helens|Staffordshire|Stockport|Stockton-on-Tees|Stoke-on-Trent|Suffolk|Sunderland|Surrey|Sutton|Swindon|Tameside|Telford and Wrekin|Thurrock|Torbay|Tower Hamlets|Trafford|Wakefield|Walsall|Waltham Forest|Wandsworth|Warrington|Warwickshire|West Berkshire|West Sussex|Westminster|Wigan|Wiltshire|Windsor and Maidenhead|Wirral|Wokingham|Wolverhampton|Worcestershire|York" +) +s_a.append( + "Artigas|Canelones|Cerro Largo|Colonia|Durazno|Flores|Florida|Lavalleja|Maldonado|Montevideo|Paysandu|Rio Negro|Rivera|Rocha|Salto|San Jose|Soriano|Tacuarembo|Treinta y Tres" +) +s_a.append( + "Alabama|Alaska|Arizona|Arkansas|California|Colorado|Connecticut|Delaware|District of Columbia|Florida|Georgia|Hawaii|Idaho|Illinois|Indiana|Iowa|Kansas|Kentucky|Louisiana|Maine|Maryland|Massachusetts|Michigan|Minnesota|Mississippi|Missouri|Montana|Nebraska|Nevada|New Hampshire|New Jersey|New Mexico|New York|North Carolina|North Dakota|Ohio|Oklahoma|Oregon|Pennsylvania|Rhode Island|South Carolina|South Dakota|Tennessee|Texas|Utah|Vermont|Virginia|Washington|West Virginia|Wisconsin|Wyoming" +) +s_a.append( + "Andijon Wiloyati|Bukhoro Wiloyati|Farghona Wiloyati|Jizzakh Wiloyati|Khorazm Wiloyati (Urganch)|Namangan Wiloyati|Nawoiy Wiloyati|Qashqadaryo Wiloyati (Qarshi)|Qoraqalpoghiston (Nukus)|Samarqand Wiloyati|Sirdaryo Wiloyati (Guliston)|Surkhondaryo Wiloyati (Termiz)|Toshkent Shahri|Toshkent Wiloyati" +) +s_a.append("Malampa|Penama|Sanma|Shefa|Tafea|Torba") +s_a.append( + "Amazonas|Anzoategui|Apure|Aragua|Barinas|Bolivar|Carabobo|Cojedes|Delta Amacuro|Dependencias Federales|Distrito Federal|Falcon|Guarico|Lara|Merida|Miranda|Monagas|Nueva Esparta|Portuguesa|Sucre|Tachira|Trujillo|Vargas|Yaracuy|Zulia" +) +s_a.append( + "An Giang|Ba Ria-Vung Tau|Bac Giang|Bac Kan|Bac Lieu|Bac Ninh|Ben Tre|Binh Dinh|Binh Duong|Binh Phuoc|Binh Thuan|Ca Mau|Can Tho|Cao Bang|Da Nang|Dac Lak|Dong Nai|Dong Thap|Gia Lai|Ha Giang|Ha Nam|Ha Noi|Ha Tay|Ha Tinh|Hai Duong|Hai Phong|Ho Chi Minh|Hoa Binh|Hung Yen|Khanh Hoa|Kien Giang|Kon Tum|Lai Chau|Lam Dong|Lang Son|Lao Cai|Long An|Nam Dinh|Nghe An|Ninh Binh|Ninh Thuan|Phu Tho|Phu Yen|Quang Binh|Quang Nam|Quang Ngai|Quang Ninh|Quang Tri|Soc Trang|Son La|Tay Ninh|Thai Binh|Thai Nguyen|Thanh Hoa|Thua Thien-Hue|Tien Giang|Tra Vinh|Tuyen Quang|Vinh Long|Vinh Phuc|Yen Bai" +) +s_a.append("Saint Croix|Saint John|Saint Thomas") +s_a.append( + "Blaenau Gwent|Bridgend|Caerphilly|Cardiff|Carmarthenshire|Ceredigion|Conwy|Denbighshire|Flintshire|Gwynedd|Isle of Anglesey|Merthyr Tydfil|Monmouthshire|Neath Port Talbot|Newport|Pembrokeshire|Powys|Rhondda Cynon Taff|Swansea|The Vale of Glamorgan|Torfaen|Wrexham" +) +s_a.append("Alo|Sigave|Wallis") +s_a.append("West Bank") +s_a.append("Western Sahara") +s_a.append( + "'Adan|'Ataq|Abyan|Al Bayda'|Al Hudaydah|Al Jawf|Al Mahrah|Al Mahwit|Dhamar|Hadhramawt|Hajjah|Ibb|Lahij|Ma'rib|Sa'dah|San'a'|Ta'izz" +) +s_a.append("Kosovo|Montenegro|Serbia|Vojvodina") +s_a.append( + "Central|Copperbelt|Eastern|Luapula|Lusaka|North-Western|Northern|Southern|Western" +) +s_a.append( + "Bulawayo|Harare|ManicalandMashonaland Central|Mashonaland East|Mashonaland West|Masvingo|Matabeleland North|Matabeleland South|Midlands" +) + +states = [] + +for state in s_a: + if "|" in state: + for item in state.split("|"): + states.append(item.replace("|", "")) + else: + states.append(state) diff --git a/base/decorators.py b/base/decorators.py new file mode 100644 index 0000000..4391f77 --- /dev/null +++ b/base/decorators.py @@ -0,0 +1,68 @@ +""" +decorator functions for base +""" + +from django.contrib import messages +from django.http import HttpResponseRedirect + +from .models import ShiftRequest, WorkTypeRequest + +decorator_with_arguments = ( + lambda decorator: lambda *args, **kwargs: lambda func: decorator( + func, *args, **kwargs + ) +) + + +@decorator_with_arguments +def shift_request_change_permission(function=None, *args, **kwargs): + def check_permission( + request, + shift_request_id=None, + *args, + **kwargs, + ): + """ + This method is used to check the employee can change a shift request or not + """ + shift_request = ShiftRequest.objects.get(id=shift_request_id) + if ( + request.user.has_perm("base.change_shiftrequest") + or request.user.employee_get + == shift_request.employee_id.employee_work_info.reporting_manager_id + or request.user.employee_get == shift_request.employee_id + ): + return function(request, *args, shift_request_id=shift_request_id, **kwargs) + messages.info(request, "You dont have permission.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + # return function(request, *args, **kwargs) + + return check_permission + + +@decorator_with_arguments +def work_type_request_change_permission(function=None, *args, **kwargs): + def check_permission( + request, + work_type_request_id=None, + *args, + **kwargs, + ): + """ + This method is used to check the employee can change a shift request or not + """ + work_type_request = WorkTypeRequest.objects.get(id=work_type_request_id) + if ( + request.user.has_perm("base.change_worktyperequest") + or request.user.employee_get + == work_type_request.employee_id.employee_work_info.reporting_manager_id + or request.user.employee_get == work_type_request.employee_id + ): + return function( + request, *args, work_type_request_id=work_type_request_id, **kwargs + ) + messages.info(request, "You dont have permission.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + # return function(request, *args, **kwargs) + + return check_permission diff --git a/base/filters.py b/base/filters.py new file mode 100644 index 0000000..0927b22 --- /dev/null +++ b/base/filters.py @@ -0,0 +1,404 @@ +""" +This module contains custom Django filters for filtering querysets related to Shift Requests, +Work Type Requests, Rotating Shift and Rotating Work Type Assign. +""" + +import uuid + +import django_filters +from django import forms +from django.utils.translation import gettext as __ +from django_filters import CharFilter, DateFilter, filters + +from base.models import ( + CompanyLeaves, + Holidays, + PenaltyAccounts, + RotatingShiftAssign, + RotatingWorkTypeAssign, + ShiftRequest, + WorkTypeRequest, +) +from horilla.filters import FilterSet, filter_by_name + + +class ShiftRequestFilter(FilterSet): + """ + Custom filter for Shift Requests. + """ + + requested_date = django_filters.DateFilter( + field_name="requested_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + requested_date__gte = django_filters.DateFilter( + field_name="requested_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + requested_date__lte = django_filters.DateFilter( + field_name="requested_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + search = CharFilter(method=filter_by_name) + + class Meta: + """ + A nested class that specifies the model and fields for the filter. + """ + + fields = "__all__" + model = ShiftRequest + fields = [ + "id", + "employee_id", + "requested_date", + "previous_shift_id", + "shift_id", + "requested_till", + "approved", + "canceled", + "employee_id__employee_first_name", + "employee_id__employee_last_name", + "employee_id__is_active", + "employee_id__gender", + "employee_id__employee_work_info__job_position_id", + "employee_id__employee_work_info__department_id", + "employee_id__employee_work_info__work_type_id", + "employee_id__employee_work_info__employee_type_id", + "employee_id__employee_work_info__job_role_id", + "employee_id__employee_work_info__reporting_manager_id", + "employee_id__employee_work_info__company_id", + "employee_id__employee_work_info__shift_id", + ] + + def __init__(self, data=None, queryset=None, *, request=None, prefix=None): + super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) + for field in self.form.fields.keys(): + self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + + +class WorkTypeRequestFilter(FilterSet): + """ + Custom filter for Work Type Requests. + """ + + requested_date = django_filters.DateFilter( + field_name="requested_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + requested_date__gte = django_filters.DateFilter( + field_name="requested_till", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + requested_date__lte = django_filters.DateFilter( + field_name="requested_till", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + search = CharFilter(method=filter_by_name) + + class Meta: + """ + A nested class that specifies the model and fields for the filter. + """ + + fields = "__all__" + model = WorkTypeRequest + fields = [ + "id", + "employee_id", + "requested_date", + "previous_work_type_id", + "approved", + "work_type_id", + "canceled", + "employee_id__employee_first_name", + "employee_id__employee_last_name", + "employee_id__is_active", + "employee_id__gender", + "employee_id__employee_work_info__job_position_id", + "employee_id__employee_work_info__department_id", + "employee_id__employee_work_info__work_type_id", + "employee_id__employee_work_info__employee_type_id", + "employee_id__employee_work_info__job_role_id", + "employee_id__employee_work_info__reporting_manager_id", + "employee_id__employee_work_info__company_id", + "employee_id__employee_work_info__shift_id", + ] + + def __init__(self, data=None, queryset=None, *, request=None, prefix=None): + super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) + for field in self.form.fields.keys(): + self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + + +class RotatingShiftAssignFilters(FilterSet): + """ + Custom filter for Rotating Shift Assign. + """ + + search = CharFilter(method=filter_by_name) + + next_change_date = django_filters.DateFilter( + field_name="next_change_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + start_date = django_filters.DateFilter( + field_name="start_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + + class Meta: + """ + A nested class that specifies the model and fields for the filter. + """ + + fields = "__all__" + model = RotatingShiftAssign + fields = [ + "employee_id", + "rotating_shift_id", + "next_change_date", + "start_date", + "based_on", + "rotate_after_day", + "rotate_every_weekend", + "rotate_every", + "current_shift", + "next_shift", + "is_active", + "employee_id__employee_work_info__job_position_id", + "employee_id__employee_work_info__department_id", + "employee_id__employee_work_info__work_type_id", + "employee_id__employee_work_info__employee_type_id", + "employee_id__employee_work_info__job_role_id", + "employee_id__employee_work_info__reporting_manager_id", + "employee_id__employee_work_info__company_id", + "employee_id__employee_work_info__shift_id", + ] + + +class RotatingWorkTypeAssignFilter(FilterSet): + """ + Custom filter for Rotating Work Type Assign. + """ + + search = CharFilter(method=filter_by_name) + + next_change_date = django_filters.DateFilter( + field_name="next_change_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + start_date = django_filters.DateFilter( + field_name="start_date", widget=forms.DateInput(attrs={"type": "date"}) + ) + + class Meta: + """ + A nested class that specifies the model and fields for the filter. + """ + + fields = "__all__" + model = RotatingWorkTypeAssign + fields = [ + "employee_id", + "rotating_work_type_id", + "next_change_date", + "start_date", + "based_on", + "rotate_after_day", + "rotate_every_weekend", + "rotate_every", + "current_work_type", + "next_work_type", + "is_active", + "employee_id__employee_work_info__job_position_id", + "employee_id__employee_work_info__department_id", + "employee_id__employee_work_info__work_type_id", + "employee_id__employee_work_info__employee_type_id", + "employee_id__employee_work_info__job_role_id", + "employee_id__employee_work_info__reporting_manager_id", + "employee_id__employee_work_info__company_id", + "employee_id__employee_work_info__shift_id", + ] + + +class ShiftRequestReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("employee_id", "Employee"), + ("shift_id", "Requested Shift"), + ("previous_shift_id", "Current Shift"), + ("requested_date", "Requested Date"), + ] + + +class WorkTypeRequestReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("employee_id", "Employee"), + ("work_type_id", "Requested Work Type"), + ("previous_work_type_id", "Current Work Type"), + ("requested_date", "Requested Date"), + ("employee_id__employee_work_info__department_id", "Department"), + ("employee_id__employee_work_info__job_position_id", "Job Position"), + ("employee_id__employee_work_info__reporting_manager_id", "Reporting Manager"), + ] + + +class RotatingWorkTypeRequestReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("employee_id", "Employee"), + ("rotating_work_type_id", "Rotating Work Type"), + ("current_work_type", "Current Work Type"), + ("based_on", "Based On"), + ("employee_id__employee_work_info__department_id", "Department"), + ("employee_id__employee_work_info__job_role_id", "Job Role"), + ("employee_id__employee_work_info__reporting_manager_id", "Reporting Manager"), + ] + + +class RotatingShiftRequestReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("employee_id", "Employee"), + ("rotating_shift_id", "Rotating Shift"), + ("based_on", "Based On"), + ("employee_id__employee_work_info__department_id", "Department"), + ("employee_id__employee_work_info__job_role_id", "Job Role"), + ("employee_id__employee_work_info__reporting_manager_id", "Reporting Manager"), + ] + + +class HolidayFilter(FilterSet): + """ + Filter class for Holidays model. + + This filter allows searching Holidays objects based on name and date range. + """ + + search = filters.CharFilter(field_name="name", lookup_expr="icontains") + from_date = DateFilter( + field_name="start_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + to_date = DateFilter( + field_name="end_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + start_date = DateFilter( + field_name="start_date", + lookup_expr="exact", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + end_date = DateFilter( + field_name="end_date", + lookup_expr="exact", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + class Meta: + """ + Meta class defines the model and fields to filter + """ + + model = Holidays + fields = { + "recurring": ["exact"], + } + + def __init__(self, data=None, queryset=None, *, request=None, prefix=None): + super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) + for field in self.form.fields.keys(): + self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + self.form.fields["from_date"].label = ( + f"{self.Meta.model()._meta.get_field('start_date').verbose_name} From" + ) + self.form.fields["to_date"].label = ( + f"{self.Meta.model()._meta.get_field('end_date').verbose_name} Till" + ) + + +class CompanyLeaveFilter(FilterSet): + """ + Filter class for CompanyLeaves model. + + This filter allows searching CompanyLeaves objects based on + name, week day and based_on_week choices. + """ + + name = filters.CharFilter(field_name="based_on_week_day", lookup_expr="icontains") + search = filters.CharFilter(method="filter_week_day") + + class Meta: + """ " + Meta class defines the model and fields to filter + """ + + model = CompanyLeaves + fields = { + "based_on_week": ["exact"], + "based_on_week_day": ["exact"], + } + + def filter_week_day(self, queryset, _, value): + week_qry = CompanyLeaves.objects.none() + weekday_values = [] + week_values = [] + WEEK_DAYS = [ + ("0", __("Monday")), + ("1", __("Tuesday")), + ("2", __("Wednesday")), + ("3", __("Thursday")), + ("4", __("Friday")), + ("5", __("Saturday")), + ("6", __("Sunday")), + ] + WEEKS = [ + (None, __("All")), + ("0", __("First Week")), + ("1", __("Second Week")), + ("2", __("Third Week")), + ("3", __("Fourth Week")), + ("4", __("Fifth Week")), + ] + + for day_value, day_name in WEEK_DAYS: + if value.lower() in day_name.lower(): + weekday_values.append(day_value) + for day_value, day_name in WEEKS: + if value.lower() in day_name.lower() and value.lower() != __("All").lower(): + week_values.append(day_value) + week_qry = queryset.filter(based_on_week__in=week_values) + elif value.lower() in __("All").lower(): + week_qry = queryset.filter(based_on_week__isnull=True) + return queryset.filter(based_on_week_day__in=weekday_values) | week_qry + + +class PenaltyFilter(FilterSet): + """ + PenaltyFilter + """ + + class Meta: + model = PenaltyAccounts + fields = "__all__" diff --git a/base/forms.py b/base/forms.py new file mode 100644 index 0000000..deac836 --- /dev/null +++ b/base/forms.py @@ -0,0 +1,2800 @@ +""" +forms.py + +This module is used to register forms for base module +""" + +import calendar +import ipaddress +import os +import uuid +from datetime import date, datetime, timedelta +from typing import Any + +from django import forms +from django.apps import apps +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.auth.forms import SetPasswordForm, _unicode_ci_compare +from django.contrib.auth.models import Group, Permission, User +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ValidationError +from django.core.mail import EmailMultiAlternatives +from django.core.validators import validate_ipv46_address +from django.forms import HiddenInput, TextInput +from django.template import loader +from django.template.loader import render_to_string +from django.utils.encoding import force_bytes +from django.utils.html import strip_tags +from django.utils.http import urlsafe_base64_encode +from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _trans + +from base.methods import reload_queryset +from base.models import ( + Announcement, + AnnouncementComment, + AnnouncementExpire, + Attachment, + AttendanceAllowedIP, + BaserequestFile, + Company, + CompanyLeaves, + Department, + DriverViewed, + DynamicEmailConfiguration, + DynamicPagination, + EmployeeShift, + EmployeeShiftDay, + EmployeeShiftSchedule, + EmployeeType, + Holidays, + HorillaMailTemplate, + JobPosition, + JobRole, + MultipleApprovalCondition, + PenaltyAccounts, + RotatingShift, + RotatingShiftAssign, + RotatingWorkType, + RotatingWorkTypeAssign, + ShiftRequest, + ShiftRequestComment, + Tags, + TrackLateComeEarlyOut, + WorkType, + WorkTypeRequest, + WorkTypeRequestComment, +) +from employee.filters import EmployeeFilter +from employee.forms import MultipleFileField +from employee.models import Employee +from horilla import horilla_middlewares +from horilla.horilla_middlewares import _thread_locals +from horilla.methods import get_horilla_model_class +from horilla_audit.models import AuditTag +from horilla_widgets.widgets.horilla_multi_select_field import HorillaMultiSelectField +from horilla_widgets.widgets.select_widgets import HorillaMultiSelectWidget + +# your form here + + +def validate_time_format(value): + """ + this method is used to validate the format of duration like fields. + """ + if len(value) > 6: + raise ValidationError(_("Invalid format, it should be HH:MM format")) + try: + hour, minute = value.split(":") + hour = int(hour) + minute = int(minute) + if len(str(hour)) > 3 or minute not in range(60): + raise ValidationError(_("Invalid format, it should be HH:MM format")) + except ValueError as error: + raise ValidationError(_("Invalid format, it should be HH:MM format")) from error + + +BASED_ON = [ + ("after", _trans("After")), + ("weekly", _trans("Weekend")), + ("monthly", _trans("Monthly")), +] + + +def get_next_week_date(target_day, start_date): + """ + Calculates the date of the next occurrence of the target day within the next week. + + Parameters: + target_day (int): The target day of the week (0-6, where Monday is 0 and Sunday is 6). + start_date (date): The starting date. + + Returns: + date: The date of the next occurrence of the target day within the next week. + """ + if start_date.weekday() == target_day: + return start_date + days_until_target_day = (target_day - start_date.weekday()) % 7 + if days_until_target_day == 0: + days_until_target_day = 7 + return start_date + timedelta(days=days_until_target_day) + + +def get_next_monthly_date(start_date, rotate_every): + """ + Given a start date and a rotation day (specified as an integer between 1 and 31, or + the string 'last'),calculates the next rotation date for a monthly rotation schedule. + + If the rotation day has not yet occurred in the current month, the next rotation date + will be on the rotation day of the current month. If the rotation day has already + occurred in the current month, the next rotation date will be on the rotation day of + the next month. + + If 'last' is specified as the rotation day, the next rotation date will be on the + last day of the current month. + + Parameters: + - start_date: The start date of the rotation schedule, as a date object. + - rotate_every: The rotation day, specified as an integer between 1 and 31, or the + string 'last'. + + Returns: + - A date object representing the next rotation date. + """ + + if rotate_every == "last": + # Set rotate_every to the last day of the current month + last_day = calendar.monthrange(start_date.year, start_date.month)[1] + rotate_every = str(last_day) + rotate_every = int(rotate_every) + + # Calculate the next change date + if start_date.day <= rotate_every or rotate_every == 0: + # If the rotation day has not occurred yet this month, or if it's the last- + # day of the month, set the next change date to the rotation day of this month + try: + next_change = date(start_date.year, start_date.month, rotate_every) + except ValueError: + next_change = date( + start_date.year, start_date.month + 1, 1 + ) # Advance to next month + # Set day to rotate_every + next_change = date(next_change.year, next_change.month, rotate_every) + else: + # If the rotation day has already occurred this month, set the next change + # date to the rotation day of the next month + last_day = calendar.monthrange(start_date.year, start_date.month)[1] + next_month_start = start_date.replace(day=last_day) + timedelta(days=1) + try: + next_change = next_month_start.replace(day=rotate_every) + except ValueError: + next_change = ( + next_month_start.replace(month=next_month_start.month + 1) + + timedelta(days=1) + ).replace(day=rotate_every) + + return next_change + + +class ModelForm(forms.ModelForm): + """ + Override of Django ModelForm to add initial styling and defaults. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + reload_queryset(self.fields) + + request = getattr(horilla_middlewares._thread_locals, "request", None) + + today = date.today() + now = datetime.now() + + default_input_class = "oh-input w-100" + select_class = "oh-select oh-select-2" + checkbox_class = "oh-switch__checkbox" + + for field_name, field in self.fields.items(): + widget = field.widget + label = _(field.label) if field.label else "" + + # Date field + if isinstance(widget, forms.DateInput): + field.initial = today + widget.input_type = "date" + widget.format = "%Y-%m-%d" + field.input_formats = ["%Y-%m-%d"] + + existing_class = widget.attrs.get("class", default_input_class) + widget.attrs.update( + { + "class": f"{existing_class} form-control", + "placeholder": label, + } + ) + + # Time field + elif isinstance(widget, forms.TimeInput): + field.initial = now.strftime("%H:%M") + widget.input_type = "time" + widget.format = "%H:%M" + field.input_formats = ["%H:%M"] + + existing_class = widget.attrs.get("class", default_input_class) + widget.attrs.update( + { + "class": f"{existing_class} form-control", + "placeholder": label, + } + ) + + # Number, Email, Text, File, URL fields + elif isinstance( + widget, + ( + forms.NumberInput, + forms.EmailInput, + forms.TextInput, + forms.FileInput, + forms.URLInput, + ), + ): + existing_class = widget.attrs.get("class", default_input_class) + widget.attrs.update( + { + "class": f"{existing_class} form-control", + "placeholder": _(field.label.title()) if field.label else "", + } + ) + + # Select fields + elif isinstance(widget, forms.Select): + if not isinstance(field, forms.ModelMultipleChoiceField): + field.empty_label = _("---Choose {label}---").format(label=label) + existing_class = widget.attrs.get("class", select_class) + widget.attrs.update({"class": existing_class}) + + # Textarea + elif isinstance(widget, forms.Textarea): + existing_class = widget.attrs.get("class", default_input_class) + widget.attrs.update( + { + "class": f"{existing_class} form-control", + "placeholder": label, + "rows": 2, + "cols": 40, + } + ) + + # Checkbox types + elif isinstance( + widget, (forms.CheckboxInput, forms.CheckboxSelectMultiple) + ): + existing_class = widget.attrs.get("class", checkbox_class) + widget.attrs.update({"class": existing_class}) + + # Set employee_id and company_id once + if request: + employee = getattr(request.user, "employee_get", None) + if employee: + if "employee_id" in self.fields: + self.fields["employee_id"].initial = employee + + if "company_id" in self.fields: + company_field = self.fields["company_id"] + company = getattr(employee, "get_company", None) + if company: + queryset = company_field.queryset + company_field.initial = ( + company if company in queryset else queryset.first() + ) + + def verbose_name(self): + """ + Returns the verbose name of the model associated with the form. + Provides fallback values if no model or verbose name is defined. + """ + if hasattr(self, "_meta") and hasattr(self._meta, "model"): + model = self._meta.model + if hasattr(model._meta, "verbose_name") and model._meta.verbose_name: + return model._meta.verbose_name + return model.__name__ + return "" + + +class Form(forms.Form): + """ + Overrides to add initial styling to the django Form instance + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name, field in self.fields.items(): + widget = field.widget + if isinstance( + widget, (forms.NumberInput, forms.EmailInput, forms.TextInput) + ): + if field.label is not None: + label = _(field.label) + field.widget.attrs.update( + {"class": "oh-input w-100", "placeholder": label} + ) + elif isinstance(widget, (forms.Select,)): + label = "" + if field.label is not None: + label = field.label.replace("id", " ") + field.empty_label = _("---Choose {label}---").format(label=label) + field.widget.attrs.update({"class": "oh-select oh-select-2"}) + elif isinstance(widget, (forms.Textarea)): + label = _(field.label) + field.widget.attrs.update( + { + "class": "oh-input w-100", + "placeholder": label, + "rows": 2, + "cols": 40, + } + ) + elif isinstance( + widget, + ( + forms.CheckboxInput, + forms.CheckboxSelectMultiple, + ), + ): + field.widget.attrs.update({"class": "oh-switch__checkbox"}) + + +class UserGroupForm(ModelForm): + """ + Django user groups form + """ + + try: + permissions = forms.MultipleChoiceField( + choices=[(perm.codename, perm.name) for perm in Permission.objects.all()], + required=False, + error_messages={ + "required": "Please choose a permission.", + }, + ) + except: + pass + + class Meta: + """ + Meta class for additional options + """ + + model = Group + fields = ["name", "permissions"] + + def save(self, commit=True): + """ + ModelForm save override + """ + group = super().save(commit=False) + if self.instance: + group = self.instance + group.save() + + # Convert the selected codenames back to Permission instances + permissions_codenames = self.cleaned_data["permissions"] + permissions = Permission.objects.filter(codename__in=permissions_codenames) + + # Set the associated permissions + group.permissions.set(permissions) + + if commit: + group.save() + + return group + + +class AssignUserGroup(Form): + """ + Form to assign groups + """ + + employee = forms.ModelMultipleChoiceField( + queryset=Employee.objects.all(), required=False + ) + group = forms.ModelChoiceField(queryset=Group.objects.all()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + + def save(self): + """ + Save method to assign group to selected employees only. + It removes the group from previously assigned employees + and assigns it to the new ones. + """ + group = self.cleaned_data["group"] + assigning_employees = self.cleaned_data["employee"] + assigning_users = [ + e.employee_user_id for e in assigning_employees if e.employee_user_id + ] + + # Get employees currently in this group on selected company instance + existing_employees = Employee.objects.filter( + employee_user_id__in=group.user_set.all() + ) + existing_users = [ + e.employee_user_id for e in existing_employees if e.employee_user_id + ] + + for user in existing_users: + user.groups.remove(group) + + for user in assigning_users: + user.groups.add(group) + + return group + + +class AssignPermission(Form): + """ + Forms to assign user permision + """ + + employee = HorillaMultiSelectField( + queryset=Employee.objects.all(), + widget=HorillaMultiSelectWidget( + filter_route_name="employee-widget-filter", + filter_class=EmployeeFilter, + filter_instance_contex_name="f", + filter_template_path="employee_filters.html", + required=True, + ), + label="Employee", + ) + try: + permissions = forms.MultipleChoiceField( + choices=[(perm.codename, perm.name) for perm in Permission.objects.all()], + error_messages={ + "required": "Please choose a permission.", + }, + ) + except: + pass + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + + def clean(self): + emps = self.data.getlist("employee") + if emps: + self.errors.pop("employee", None) + super().clean() + return + + def save(self): + """ + Save method to assign permission to employee + """ + user_ids = Employee.objects.filter( + id__in=self.data.getlist("employee") + ).values_list("employee_user_id", flat=True) + permissions = self.cleaned_data["permissions"] + permissions = Permission.objects.filter(codename__in=permissions) + users = User.objects.filter(id__in=user_ids) + for user in users: + user.user_permissions.set(permissions) + + return self + + +class CompanyForm(ModelForm): + """ + Company model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = Company + fields = "__all__" + exclude = ["date_format", "time_format", "is_active"] + + def validate_image(self, file): + max_size = 5 * 1024 * 1024 + + if file.size > max_size: + raise ValidationError("File size should be less than 5MB.") + + # Check file extension + valid_extensions = [".jpg", ".jpeg", ".png", ".webp", ".svg"] + ext = os.path.splitext(file.name)[1].lower() + if ext not in valid_extensions: + raise ValidationError("Unsupported file extension.") + + def clean_icon(self): + icon = self.cleaned_data.get("icon") + if icon: + self.validate_image(icon) + return icon + + +class DepartmentForm(ModelForm): + """ + Department model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = Department + fields = "__all__" + exclude = ["is_active"] + + +class JobPositionForm(ModelForm): + """ + JobPosition model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = JobPosition + fields = "__all__" + exclude = ["is_active"] + + +class JobPositionMultiForm(ModelForm): + """ + JobPosition model's form + """ + + department_id = HorillaMultiSelectField( + queryset=Department.objects.all(), + label=JobPosition._meta.get_field("department_id").verbose_name, + widget=forms.SelectMultiple( + attrs={ + "class": "oh-select oh-select2 w-100", + "style": "height:45px;", + } + ), + ) + + class Meta: + model = JobPosition + fields = "__all__" + exclude = ["department_id", "is_active"] + + def clean(self): + """ + Validate that the job position does not already exist in the selected departments. + """ + cleaned_data = super().clean() + department_ids = self.data.getlist("department_id") + job_position = self.data.get("job_position") + + existing_positions = JobPosition.objects.filter( + department_id__in=department_ids, job_position=job_position + ) + + if existing_positions.exists(): + existing_deps = existing_positions.values_list("department_id", flat=True) + dep_names = Department.objects.filter(id__in=existing_deps).values_list( + "department", flat=True + ) + raise ValidationError( + { + "department_id": _("Job position already exists under {}").format( + ", ".join(dep_names) + ) + } + ) + return cleaned_data + + def save(self, *args, **kwargs): + """ + Save the job positions for each selected department. + """ + if not self.instance.pk: + request = getattr(_thread_locals, "request") + department_ids = self.data.getlist("department_id") + job_position = self.data.get("job_position") + positions = [] + + for dep_id in department_ids: + dep = Department.objects.get(id=dep_id) + if JobPosition.objects.filter( + department_id=dep, job_position=job_position + ).exists(): + messages.error(request, f"Job position already exists under {dep}") + else: + position = JobPosition(department_id=dep, job_position=job_position) + position.save() + positions.append(position.pk) + + return JobPosition.objects.filter(id__in=positions) + return super().save(*args, **kwargs) + + +class JobRoleForm(ModelForm): + """ + JobRole model's form + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.instance.pk: + self.fields["job_position_id"] = forms.ModelMultipleChoiceField( + queryset=self.fields["job_position_id"].queryset, + label=JobRole._meta.get_field("job_position_id").verbose_name, + ) + attrs = self.fields["job_position_id"].widget.attrs + attrs["class"] = "oh-select oh-select2 w-100" + attrs["style"] = "height:45px;" + + class Meta: + """ + Meta class for additional options + """ + + model = JobRole + fields = "__all__" + exclude = ["is_active"] + + def save(self, commit, *args, **kwargs) -> Any: + if not self.instance.pk: + request = getattr(_thread_locals, "request") + job_positions = JobPosition.objects.filter( + id__in=self.data.getlist("job_position_id") + ) + roles = [] + for position in job_positions: + role = JobRole() + role.job_position_id = position + role.job_role = self.data["job_role"] + try: + role.save() + except: + messages.info(request, f"Role already exists under {position}") + roles.append(role.pk) + return JobRole.objects.filter(id__in=roles) + super().save(commit, *args, **kwargs) + + +class WorkTypeForm(ModelForm): + """ + WorkType model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = WorkType + fields = "__all__" + exclude = ["is_active"] + + +class RotatingWorkTypeForm(ModelForm): + """ + RotatingWorkType model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingWorkType + fields = "__all__" + exclude = ["employee_id", "is_active"] + widgets = { + "additional_data": forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + work_type_counts = 0 + + def create_work_type_field(work_type_key, required, initial=None): + self.fields[work_type_key] = forms.ModelChoiceField( + queryset=WorkType.objects.all(), + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2 mb-3", + "name": work_type_key, + "id": f"id_{work_type_key}", + } + ), + required=required, + empty_label=_("---Choose Work Type---"), + initial=initial, + ) + + for key in self.data.keys(): + if key.startswith("work_type"): + work_type_counts += 1 + create_work_type_field(key, work_type_counts <= 2) + + additional_data = self.initial.get("additional_data") + additional_work_types = ( + additional_data.get("additional_work_types") if additional_data else None + ) + if additional_work_types: + work_type_counts = 3 + for work_type_id in additional_work_types: + create_work_type_field( + f"work_type{work_type_counts}", + work_type_counts <= 2, + initial=work_type_id, + ) + work_type_counts += 1 + + self.work_type_counts = work_type_counts + + def as_p(self, *args, **kwargs): + context = {"form": self} + return render_to_string( + "base/rotating_work_type/htmx/rotating_work_type_as_p.html", context + ) + + def clean(self): + cleaned_data = super().clean() + additional_work_types = [] + model_fields = list(self.instance.__dict__.keys()) + + for key, value in self.data.items(): + if ( + f"{key}_id" not in model_fields + and key.startswith("work_type") + and value + ): + additional_work_types.append(value) + + if additional_work_types: + if ( + "additional_data" not in cleaned_data + or cleaned_data["additional_data"] is None + ): + cleaned_data["additional_data"] = {} + cleaned_data["additional_data"][ + "additional_work_types" + ] = additional_work_types + + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + if self.cleaned_data.get("additional_data"): + if instance.additional_data is None: + instance.additional_data = {} + instance.additional_data["additional_work_types"] = self.cleaned_data[ + "additional_data" + ].get("additional_work_types") + else: + instance.additional_data = None + + if commit: + instance.save() + self.save_m2m() + return instance + + +class RotatingWorkTypeAssignForm(ModelForm): + """ + RotatingWorkTypeAssign model's form + """ + + employee_id = HorillaMultiSelectField( + queryset=Employee.objects.filter(employee_work_info__isnull=False), + widget=HorillaMultiSelectWidget( + filter_route_name="employee-widget-filter", + filter_class=EmployeeFilter, + filter_instance_contex_name="f", + filter_template_path="employee_filters.html", + ), + label=_trans("Employees"), + ) + based_on = forms.ChoiceField( + choices=BASED_ON, initial="daily", label=_trans("Based on") + ) + rotate_after_day = forms.IntegerField(initial=5, label=_trans("Rotate after day")) + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingWorkTypeAssign + fields = "__all__" + exclude = [ + "next_change_date", + "current_work_type", + "next_work_type", + "is_active", + "additional_data", + ] + widgets = { + "is_active": HiddenInput(), + } + labels = { + "is_active": _trans("Is Active"), + "rotate_every_weekend": _trans("Rotate every weekend"), + "rotate_every": _trans("Rotate every"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + for field_name, field in self.fields.items(): + if field.required: + self.fields[field_name].label_suffix = " *" + + self.fields["rotate_every_weekend"].widget.attrs.update( + { + "class": "w-100", + "style": "display:none; height:50px; border-radius:0;border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_every"].widget.attrs.update( + { + "class": "w-100", + "style": "display:none; height:50px; border-radius:0;border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_after_day"].widget.attrs.update( + { + "class": "w-100 oh-input", + "style": " height:50px; border-radius:0;", + } + ) + self.fields["based_on"].widget.attrs.update( + { + "class": "w-100", + "style": " height:50px; border-radius:0;border:1px solid hsl(213deg,22%,84%);", + } + ) + self.fields["rotating_work_type_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + self.fields["employee_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + + def clean_employee_id(self): + employee_ids = self.cleaned_data.get("employee_id") + if employee_ids: + return employee_ids[0] + else: + return ValidationError(_("This field is required")) + + def clean(self): + super().clean() + self.instance.employee_id = Employee.objects.filter( + id=self.data.get("employee_id") + ).first() + + self.errors.pop("employee_id", None) + if self.instance.employee_id is None: + raise ValidationError({"employee_id": _("This field is required")}) + super().clean() + cleaned_data = super().clean() + if "rotate_after_day" in self.errors: + del self.errors["rotate_after_day"] + return cleaned_data + + def save(self, commit=False, manager=None): + employee_ids = self.data.getlist("employee_id") + rotating_work_type = RotatingWorkType.objects.get( + id=self.data["rotating_work_type_id"] + ) + + day_name = self.cleaned_data["rotate_every_weekend"] + day_names = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + target_day = day_names.index(day_name.lower()) + + for employee_id in employee_ids: + employee = Employee.objects.filter(id=employee_id).first() + rotating_work_type_assign = RotatingWorkTypeAssign() + rotating_work_type_assign.rotating_work_type_id = rotating_work_type + rotating_work_type_assign.employee_id = employee + rotating_work_type_assign.based_on = self.cleaned_data["based_on"] + rotating_work_type_assign.start_date = self.cleaned_data["start_date"] + rotating_work_type_assign.next_change_date = self.cleaned_data["start_date"] + rotating_work_type_assign.rotate_after_day = self.data.get( + "rotate_after_day" + ) + rotating_work_type_assign.rotate_every = self.cleaned_data["rotate_every"] + rotating_work_type_assign.rotate_every_weekend = self.cleaned_data[ + "rotate_every_weekend" + ] + rotating_work_type_assign.next_change_date = self.cleaned_data["start_date"] + rotating_work_type_assign.current_work_type = ( + employee.employee_work_info.work_type_id + ) + rotating_work_type_assign.next_work_type = rotating_work_type.work_type1 + rotating_work_type_assign.additional_data["next_work_type_index"] = 1 + based_on = self.cleaned_data["based_on"] + start_date = self.cleaned_data["start_date"] + if based_on == "weekly": + next_date = get_next_week_date(target_day, start_date) + rotating_work_type_assign.next_change_date = next_date + elif based_on == "monthly": + # 0, 1, 2, ..., 31, or "last" + rotate_every = self.cleaned_data["rotate_every"] + start_date = self.cleaned_data["start_date"] + next_date = get_next_monthly_date(start_date, rotate_every) + rotating_work_type_assign.next_change_date = next_date + elif based_on == "after": + rotating_work_type_assign.next_change_date = ( + rotating_work_type_assign.start_date + + timedelta(days=int(self.data.get("rotate_after_day"))) + ) + + rotating_work_type_assign.save() + + +class RotatingWorkTypeAssignUpdateForm(ModelForm): + """ + RotatingWorkTypeAssign model's form + """ + + based_on = forms.ChoiceField( + choices=BASED_ON, initial="daily", label=_trans("Based on") + ) + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingWorkTypeAssign + fields = "__all__" + exclude = [ + "next_change_date", + "current_work_type", + "next_work_type", + "is_active", + "additional_data", + ] + labels = { + "start_date": _trans("Start date"), + "rotate_after_day": _trans("Rotate after day"), + "rotate_every_weekend": _trans("Rotate every weekend"), + "rotate_every": _trans("Rotate every"), + "based_on": _trans("Based on"), + "is_active": _trans("Is Active"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + for field_name, field in self.fields.items(): + if field.required: + self.fields[field_name].label_suffix = " *" + + self.fields["rotate_every_weekend"].widget.attrs.update( + { + "class": "w-100", + "style": "display:none; height:50px; border-radius:0;border:1px\ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_every"].widget.attrs.update( + { + "class": "w-100", + "style": "display:none; height:50px; border-radius:0;border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_after_day"].widget.attrs.update( + { + "class": "w-100 oh-input", + "style": " height:50px; border-radius:0;", + } + ) + self.fields["based_on"].widget.attrs.update( + { + "class": "w-100", + "style": " height:50px; border-radius:0; border:1px solid \ + hsl(213deg,22%,84%);", + } + ) + self.fields["rotating_work_type_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + self.fields["employee_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + + def save(self, *args, **kwargs): + day_name = self.cleaned_data["rotate_every_weekend"] + day_names = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + target_day = day_names.index(day_name.lower()) + + based_on = self.cleaned_data["based_on"] + start_date = self.instance.start_date + if based_on == "weekly": + next_date = get_next_week_date(target_day, start_date) + self.instance.next_change_date = next_date + elif based_on == "monthly": + rotate_every = self.instance.rotate_every # 0, 1, 2, ..., 31, or "last" + start_date = self.instance.start_date + next_date = get_next_monthly_date(start_date, rotate_every) + self.instance.next_change_date = next_date + elif based_on == "after": + self.instance.next_change_date = self.instance.start_date + timedelta( + days=int(self.data.get("rotate_after_day")) + ) + return super().save() + + +class EmployeeTypeForm(ModelForm): + """ + EmployeeType form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = EmployeeType + fields = "__all__" + exclude = ["is_active"] + + +class EmployeeShiftForm(ModelForm): + """ + EmployeeShift Form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = EmployeeShift + fields = "__all__" + exclude = ["days", "is_active"] + + def clean(self): + full_time = self.data["full_time"] + validate_time_format(full_time) + full_time = self.data["weekly_full_time"] + validate_time_format(full_time) + return super().clean() + + +class EmployeeShiftScheduleUpdateForm(ModelForm): + """ + EmployeeShiftSchedule model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = EmployeeShiftSchedule + fields = "__all__" + exclude = ["is_active", "is_night_shift"] + widgets = { + "start_time": forms.TimeInput(attrs={"type": "time"}), + "end_time": forms.TimeInput(attrs={"type": "time"}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + instance = kwargs.get("instance") + + if instance: + self.fields["start_time"].initial = ( + instance.start_time.strftime("%H:%M") if instance.start_time else None + ) + self.fields["end_time"].initial = ( + instance.end_time.strftime("%H:%M") if instance.end_time else None + ) + if apps.is_installed("attendance"): + self.fields["auto_punch_out_time"].initial = ( + instance.auto_punch_out_time.strftime("%H:%M") + if instance.auto_punch_out_time + else None + ) + + if not apps.is_installed("attendance"): + self.fields.pop("auto_punch_out_time", None) + self.fields.pop("is_auto_punch_out_enabled", None) + else: + self.fields["auto_punch_out_time"].widget = forms.TimeInput( + attrs={"type": "time", "class": "oh-input w-100 form-control"} + ) + self.fields["is_auto_punch_out_enabled"].widget.attrs.update( + {"onchange": "toggleDivVisibility(this)"} + ) + + 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 clean(self): + cleaned_data = super().clean() + if apps.is_installed("attendance"): + auto_punch_out_enabled = cleaned_data.get("is_auto_punch_out_enabled") + auto_punch_out_time = cleaned_data.get("auto_punch_out_time") + end_time = cleaned_data.get("end_time") + + if auto_punch_out_enabled: + if not auto_punch_out_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time is required when automatic punch out is enabled." + ) + } + ) + elif auto_punch_out_time < end_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time cannot be earlier than the end time." + ) + } + ) + + return cleaned_data + + +class EmployeeShiftScheduleForm(ModelForm): + """ + EmployeeShiftSchedule model's form + """ + + day = forms.ModelMultipleChoiceField( + queryset=EmployeeShiftDay.objects.all(), + ) + + class Meta: + """ + Meta class for additional options + """ + + model = EmployeeShiftSchedule + fields = "__all__" + exclude = ["is_night_shift", "is_active"] + + def __init__(self, *args, **kwargs): + if instance := kwargs.get("instance"): + # """ + # django forms not showing value inside the date, time html element. + # so here overriding default forms instance method to set initial value + # """ + initial = { + "start_time": instance.start_time.strftime("%H:%M"), + "end_time": instance.end_time.strftime("%H:%M"), + } + if apps.is_installed("attendance"): + initial["auto_punch_out_time"] = ( + instance.auto_punch_out_time.strftime("%H:%M") + if instance.auto_punch_out_time + else None + ) + kwargs["initial"] = initial + super().__init__(*args, **kwargs) + self.fields["day"].widget.attrs.update({"id": str(uuid.uuid4())}) + self.fields["shift_id"].widget.attrs.update({"id": str(uuid.uuid4())}) + if not apps.is_installed("attendance"): + self.fields.pop("auto_punch_out_time", None) + self.fields.pop("is_auto_punch_out_enabled", None) + else: + self.fields["auto_punch_out_time"].widget = forms.TimeInput( + attrs={"type": "time", "class": "oh-input w-100 form-control"} + ) + self.fields["is_auto_punch_out_enabled"].widget.attrs.update( + {"onchange": "toggleDivVisibility(this)"} + ) + + 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 clean(self): + cleaned_data = super().clean() + if apps.is_installed("attendance"): + auto_punch_out_enabled = self.cleaned_data["is_auto_punch_out_enabled"] + auto_punch_out_time = self.cleaned_data["auto_punch_out_time"] + end_time = self.cleaned_data["end_time"] + if auto_punch_out_enabled: + if not auto_punch_out_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time is required when automatic punch out is enabled." + ) + } + ) + if auto_punch_out_enabled and auto_punch_out_time and end_time: + if auto_punch_out_time < end_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time cannot be earlier than the end time." + ) + } + ) + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + for day in self.data.getlist("day"): + if int(day) != int(instance.day.id): + data_copy = self.data.copy() + data_copy.update({"day": str(day)}) + shift_schedule = EmployeeShiftScheduleUpdateForm(data_copy).save( + commit=False + ) + shift_schedule.save() + if commit: + instance.save() + return instance + + def clean_day(self): + """ + Validation to day field + """ + days = self.cleaned_data["day"] + for day in days: + attendance = EmployeeShiftSchedule.objects.filter( + day=day, shift_id=self.data["shift_id"] + ).first() + if attendance is not None: + raise ValidationError( + _("Shift schedule is already exist for {day}").format( + day=_(day.day) + ) + ) + if days.first() is None: + raise ValidationError(_("Employee not chosen")) + + return days.first() + + +class RotatingShiftForm(ModelForm): + """ + RotatingShift model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingShift + fields = "__all__" + exclude = ["employee_id", "is_active"] + widgets = {"additional_data": forms.HiddenInput()} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + shift_counts = 0 + + def create_shift_field(shift_key, required, initial=None): + self.fields[shift_key] = forms.ModelChoiceField( + queryset=EmployeeShift.objects.all(), + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2 mb-3", + "name": shift_key, + "id": f"id_{shift_key}", + } + ), + required=required, + empty_label=_("---Choose Shift---"), + initial=initial, + ) + + for field in self.fields: + if field.startswith("shift"): + shift_counts += 1 + create_shift_field(field, shift_counts <= 2) + + for key in self.data.keys(): + if key.startswith("shift") and self.data[key]: + shift_counts += 1 + create_shift_field(key, shift_counts <= 2) + + additional_data = self.initial.get("additional_data") + additional_shifts = ( + additional_data.get("additional_shifts") if additional_data else None + ) + if additional_shifts: + shift_counts = 3 + for shift_id in additional_shifts: + if shift_id: + create_shift_field( + f"shift{shift_counts}", shift_counts <= 2, initial=shift_id + ) + shift_counts += 1 + + self.shift_counts = shift_counts + + def as_p(self, *args, **kwargs): + context = {"form": self} + return render_to_string( + "base/rotating_shift/htmx/rotating_shift_as_p.html", context + ) + + def clean(self): + cleaned_data = super().clean() + additional_shifts = [] + model_fields = list(self.instance.__dict__.keys()) + + for key, value in self.data.items(): + if f"{key}_id" not in model_fields and key.startswith("shift") and value: + additional_shifts.append(value) + + if additional_shifts: + if ( + "additional_data" not in cleaned_data + or cleaned_data["additional_data"] is None + ): + cleaned_data["additional_data"] = {} + cleaned_data["additional_data"]["additional_shifts"] = additional_shifts + + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + if self.cleaned_data.get("additional_data"): + if instance.additional_data is None: + instance.additional_data = {} + instance.additional_data["additional_shifts"] = self.cleaned_data[ + "additional_data" + ].get("additional_shifts") + else: + instance.additional_data = None + + if commit: + instance.save() + self.save_m2m() + return instance + + +class RotatingShiftAssignForm(ModelForm): + """ + RotatingShiftAssign model's form + """ + + employee_id = HorillaMultiSelectField( + queryset=Employee.objects.filter(employee_work_info__isnull=False), + widget=HorillaMultiSelectWidget( + filter_route_name="employee-widget-filter", + filter_class=EmployeeFilter, + filter_instance_contex_name="f", + filter_template_path="employee_filters.html", + ), + label=_trans("Employees"), + ) + based_on = forms.ChoiceField( + choices=BASED_ON, initial="daily", label=_trans("Based on") + ) + rotate_after_day = forms.IntegerField(initial=5, label=_trans("Rotate after day")) + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingShiftAssign + fields = "__all__" + exclude = [ + "next_change_date", + "current_shift", + "next_shift", + "is_active", + "additional_data", + ] + labels = { + "rotating_shift_id": _trans("Rotating Shift"), + "start_date": _("Start date"), + "is_active": _trans("Is Active"), + "rotate_every_weekend": _trans("Rotate every weekend"), + "rotate_every": _trans("Rotate every"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + for field_name, field in self.fields.items(): + if field.required: + self.fields[field_name].label_suffix = " *" + + self.fields["rotate_every_weekend"].widget.attrs.update( + { + "class": "w-100 ", + "style": "display:none; height:50px; border-radius:0;border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_every"].widget.attrs.update( + { + "class": "w-100 ", + "style": "display:none; height:50px; border-radius:0;border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_after_day"].widget.attrs.update( + { + "class": "w-100 oh-input", + "style": " height:50px; border-radius:0;", + } + ) + self.fields["based_on"].widget.attrs.update( + { + "class": "w-100", + "style": " height:50px; border-radius:0;border:1px solid hsl(213deg,22%,84%);", + } + ) + self.fields["rotating_shift_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + self.fields["employee_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + + def clean_employee_id(self): + """ + Validation to employee_id field + """ + employee_ids = self.cleaned_data.get("employee_id") + if employee_ids: + return employee_ids[0] + else: + return ValidationError(_("This field is required")) + + def clean(self): + super().clean() + self.instance.employee_id = Employee.objects.filter( + id=self.data.get("employee_id") + ).first() + + self.errors.pop("employee_id", None) + if self.instance.employee_id is None: + raise ValidationError({"employee_id": _("This field is required")}) + super().clean() + cleaned_data = super().clean() + if "rotate_after_day" in self.errors: + del self.errors["rotate_after_day"] + return cleaned_data + + def save( + self, + commit=False, + ): + employee_ids = self.data.getlist("employee_id") + rotating_shift = RotatingShift.objects.get(id=self.data["rotating_shift_id"]) + + day_name = self.cleaned_data["rotate_every_weekend"] + day_names = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + target_day = day_names.index(day_name.lower()) + for employee_id in employee_ids: + employee = Employee.objects.filter(id=employee_id).first() + rotating_shift_assign = RotatingShiftAssign() + rotating_shift_assign.rotating_shift_id = rotating_shift + rotating_shift_assign.employee_id = employee + rotating_shift_assign.based_on = self.cleaned_data["based_on"] + rotating_shift_assign.start_date = self.cleaned_data["start_date"] + rotating_shift_assign.next_change_date = self.cleaned_data["start_date"] + rotating_shift_assign.rotate_after_day = self.data.get("rotate_after_day") + rotating_shift_assign.rotate_every = self.cleaned_data["rotate_every"] + rotating_shift_assign.rotate_every_weekend = self.cleaned_data[ + "rotate_every_weekend" + ] + rotating_shift_assign.next_change_date = self.cleaned_data["start_date"] + rotating_shift_assign.current_shift = employee.employee_work_info.shift_id + rotating_shift_assign.next_shift = rotating_shift.shift1 + rotating_shift_assign.additional_data["next_shift_index"] = 1 + based_on = self.cleaned_data["based_on"] + start_date = self.cleaned_data["start_date"] + if based_on == "weekly": + next_date = get_next_week_date(target_day, start_date) + rotating_shift_assign.next_change_date = next_date + elif based_on == "monthly": + # 0, 1, 2, ..., 31, or "last" + rotate_every = self.cleaned_data["rotate_every"] + start_date = self.cleaned_data["start_date"] + next_date = get_next_monthly_date(start_date, rotate_every) + rotating_shift_assign.next_change_date = next_date + elif based_on == "after": + rotating_shift_assign.next_change_date = ( + rotating_shift_assign.start_date + + timedelta(days=int(self.data.get("rotate_after_day"))) + ) + rotating_shift_assign.save() + + +class RotatingShiftAssignUpdateForm(ModelForm): + """ + RotatingShiftAssign model's form + """ + + based_on = forms.ChoiceField( + choices=BASED_ON, initial="daily", label=_trans("Based on") + ) + + class Meta: + """ + Meta class for additional options + """ + + model = RotatingShiftAssign + fields = "__all__" + exclude = [ + "next_change_date", + "current_shift", + "next_shift", + "is_active", + "additional_data", + ] + labels = { + "start_date": _trans("Start date"), + "rotate_after_day": _trans("Rotate after day"), + "rotate_every_weekend": _trans("Rotate every weekend"), + "rotate_every": _trans("Rotate every"), + "based_on": _trans("Based on"), + "is_active": _trans("Is Active"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + for field_name, field in self.fields.items(): + if field.required: + self.fields[field_name].label_suffix = " *" + + self.fields["rotate_every_weekend"].widget.attrs.update( + { + "class": "w-100 ", + "style": "display:none; height:50px; border-radius:0; border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_every"].widget.attrs.update( + { + "class": "w-100 ", + "style": "display:none; height:50px; border-radius:0; border:1px \ + solid hsl(213deg,22%,84%);", + "data-hidden": True, + } + ) + self.fields["rotate_after_day"].widget.attrs.update( + { + "class": "w-100 oh-input", + "style": " height:50px; border-radius:0;", + } + ) + self.fields["based_on"].widget.attrs.update( + { + "class": "w-100", + "style": " height:50px; border-radius:0; border:1px solid hsl(213deg,22%,84%);", + } + ) + self.fields["rotating_shift_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + self.fields["employee_id"].widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + + def save(self, *args, **kwargs): + day_name = self.cleaned_data["rotate_every_weekend"] + day_names = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + target_day = day_names.index(day_name.lower()) + + based_on = self.cleaned_data["based_on"] + start_date = self.instance.start_date + if based_on == "weekly": + next_date = get_next_week_date(target_day, start_date) + self.instance.next_change_date = next_date + elif based_on == "monthly": + rotate_every = self.instance.rotate_every # 0, 1, 2, ..., 31, or "last" + start_date = self.instance.start_date + next_date = get_next_monthly_date(start_date, rotate_every) + self.instance.next_change_date = next_date + elif based_on == "after": + self.instance.next_change_date = self.instance.start_date + timedelta( + days=int(self.data.get("rotate_after_day")) + ) + return super().save() + + +class ShiftRequestForm(ModelForm): + """ + ShiftRequest model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = ShiftRequest + fields = "__all__" + exclude = [ + "reallocate_to", + "approved", + "canceled", + "reallocate_approved", + "reallocate_canceled", + "previous_shift_id", + "is_active", + "shift_changed", + ] + labels = { + "description": _trans("Description"), + "requested_date": _trans("Requested Date"), + "requested_till": _trans("Requested Till"), + } + + 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 = ...): + if not self.instance.approved: + employee = self.instance.employee_id + if hasattr(employee, "employee_work_info"): + self.instance.previous_shift_id = employee.employee_work_info.shift_id + if self.instance.is_permanent_shift: + self.instance.requested_till = None + return super().save(commit) + + # here set default filter for all the employees those have work information filled. + + +class ShiftAllocationForm(ModelForm): + """ + ShiftRequest model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = ShiftRequest + fields = "__all__" + exclude = ( + "is_permanent_shift", + "approved", + "canceled", + "reallocate_approved", + "reallocate_canceled", + "previous_shift_id", + "is_active", + "shift_changed", + ) + + labels = { + "description": _trans("Description"), + "requested_date": _trans("Requested Date"), + "requested_till": _trans("Requested Till"), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["requested_till"].required = True + self.fields["requested_till"].widget.attrs.update({"required": True}) + self.fields["shift_id"].widget.attrs.update( + { + "hx-target": "#id_reallocate_to_parent_div", + "hx-trigger": "change", + "hx-get": "/update-employee-allocation", + } + ) + + 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 = ...): + if not self.instance.approved: + employee = self.instance.employee_id + if hasattr(employee, "employee_work_info"): + self.instance.previous_shift_id = employee.employee_work_info.shift_id + if not self.instance.requested_till: + self.instance.requested_till = ( + employee.employee_work_info.contract_end_date + ) + return super().save(commit) + + +class WorkTypeRequestForm(ModelForm): + """ + WorkTypeRequest model's form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = WorkTypeRequest + fields = "__all__" + exclude = ( + "approved", + "canceled", + "previous_work_type_id", + "is_active", + "work_type_changed", + ) + labels = { + "requested_date": _trans("Requested Date"), + "requested_till": _trans("Requested Till"), + "description": _trans("Description"), + } + + 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 = ...): + if not self.instance.approved: + employee = self.instance.employee_id + if hasattr(employee, "employee_work_info"): + self.instance.previous_work_type_id = ( + employee.employee_work_info.work_type_id + ) + if self.instance.is_permanent_work_type: + self.instance.requested_till = None + return super().save(commit) + + +class ChangePasswordForm(forms.Form): + old_password = forms.CharField( + label=_("Old password"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password", + "placeholder": _("Enter Old Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + help_text=_("Enter your old password."), + ) + new_password = forms.CharField( + label=_("New password"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password", + "placeholder": _("Enter New Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + ) + confirm_password = forms.CharField( + label=_("New password confirmation"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password", + "placeholder": _("Re-Enter Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + ) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(ChangePasswordForm, self).__init__(*args, **kwargs) + + def clean_old_password(self): + old_password = self.cleaned_data.get("old_password") + if not self.user.check_password(old_password): + raise forms.ValidationError("Incorrect old password.") + return old_password + + def clean_new_password(self): + new_password = self.cleaned_data.get("new_password") + if self.user.check_password(new_password): + raise forms.ValidationError( + "New password must be different from the old password." + ) + + return new_password + + def clean(self): + cleaned_data = super().clean() + new_password = cleaned_data.get("new_password") + confirm_password = cleaned_data.get("confirm_password") + if new_password and confirm_password and new_password != confirm_password: + raise ValidationError( + {"new_password": _("New password and confirm password do not match")} + ) + + return cleaned_data + + +class ChangeUsernameForm(forms.Form): + old_username = forms.CharField( + label=_("Old Username"), + strip=False, + widget=forms.TextInput( + attrs={ + "readonly": "readonly", + "class": "oh-input oh-input--text w-100 mb-2", + } + ), + ) + + username = forms.CharField( + label=_("Username"), + strip=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("Enter New Username"), + "class": "oh-input oh-input--text w-100 mb-2", + } + ), + help_text=_("Enter your username."), + ) + + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "placeholder": _("Enter Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + help_text=_("Enter your password."), + ) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(ChangeUsernameForm, self).__init__(*args, **kwargs) + + def clean_password(self): + username = self.cleaned_data.get("username") + if User.objects.filter(username=username).exists(): + raise forms.ValidationError("Username already exists.") + password = self.cleaned_data.get("password") + if not self.user.check_password(password): + raise forms.ValidationError("Incorrect password.") + return password + + +class ResetPasswordForm(SetPasswordForm): + """ + ResetPasswordForm + """ + + new_password1 = forms.CharField( + label=_("New password"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password", + "placeholder": _("Enter Strong Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + help_text=_("Enter your new password."), + ) + new_password2 = forms.CharField( + label=_("New password confirmation"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password", + "placeholder": _("Re-Enter Password"), + "class": "oh-input oh-input--password w-100 mb-2", + } + ), + help_text=_("Enter the same password as before, for verification."), + ) + + def save(self, commit=True): + if self.is_valid(): + request = getattr(_thread_locals, "request", None) + if request: + messages.success(request, _("Password changed successfully")) + return super().save() + + def clean_confirm_password(self): + """ + validation method for confirm password field + """ + password = self.cleaned_data.get("password") + confirm_password = self.cleaned_data.get("confirm_password") + if password == confirm_password: + return confirm_password + raise forms.ValidationError(_("Password must be same.")) + + +excluded_fields = [ + "id", + "is_active", + "reallocate_approved", + "reallocate_canceled", + "shift_changed", + "work_type_changed", + "created_at", + "created_by", + "modified_by", + "additional_data", + "horilla_history", + "additional_data", +] + + +class ShiftRequestColumnForm(forms.Form): + model_fields = ShiftRequest._meta.get_fields() + field_choices = [ + (field.name, field.verbose_name) + for field in model_fields + if hasattr(field, "verbose_name") and field.name not in excluded_fields + ] + selected_fields = forms.MultipleChoiceField( + choices=field_choices, + widget=forms.CheckboxSelectMultiple, + initial=[ + "employee_id", + "shift_id", + "requested_date", + "requested_till", + "previous_shift_id", + "approved", + ], + ) + + +class WorkTypeRequestColumnForm(forms.Form): + model_fields = WorkTypeRequest._meta.get_fields() + field_choices = [ + (field.name, field.verbose_name) + for field in model_fields + if hasattr(field, "verbose_name") and field.name not in excluded_fields + ] + selected_fields = forms.MultipleChoiceField( + choices=field_choices, + widget=forms.CheckboxSelectMultiple, + initial=[ + "employee_id", + "work_type_id", + "requested_date", + "requested_till", + "previous_shift_id", + "approved", + ], + ) + + +class RotatingShiftAssignExportForm(forms.Form): + model_fields = RotatingShiftAssign._meta.get_fields() + field_choices = [ + (field.name, field.verbose_name) + for field in model_fields + if hasattr(field, "verbose_name") and field.name not in excluded_fields + ] + selected_fields = forms.MultipleChoiceField( + choices=field_choices, + widget=forms.CheckboxSelectMultiple, + initial=[ + "employee_id", + "rotating_shift_id", + "start_date", + "next_change_date", + "current_shift", + "next_shift", + "based_on", + ], + ) + + +class RotatingWorkTypeAssignExportForm(forms.Form): + model_fields = RotatingWorkTypeAssign._meta.get_fields() + field_choices = [ + (field.name, field.verbose_name) + for field in model_fields + if hasattr(field, "verbose_name") and field.name not in excluded_fields + ] + selected_fields = forms.MultipleChoiceField( + choices=field_choices, + widget=forms.CheckboxSelectMultiple, + initial=[ + "employee_id", + "rotating_work_type_id", + "start_date", + "next_change_date", + "current_work_type", + "next_work_type", + "based_on", + ], + ) + + +class TagsForm(ModelForm): + """ + Tags form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = Tags + fields = "__all__" + widgets = {"color": TextInput(attrs={"type": "color", "style": "height:50px"})} + exclude = ["objects", "is_active"] + + def as_p(self, *args, **kwargs): + """ + 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 + + +class AuditTagForm(ModelForm): + """ + Audit Tags form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = AuditTag + fields = "__all__" + + +class ShiftRequestCommentForm(ModelForm): + """ + Shift request comment form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = ShiftRequestComment + fields = ("comment",) + + +class WorkTypeRequestCommentForm(ModelForm): + """ + WorkType request comment form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = WorkTypeRequestComment + fields = ("comment",) + + +class DynamicMailConfForm(ModelForm): + """ + DynamicEmailConfiguration + """ + + class Meta: + model = DynamicEmailConfiguration + fields = "__all__" + exclude = ["is_active"] + + # def clean(self): + # from_mail = self.from_email + # return super().clean() + 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 + + +class DynamicMailTestForm(forms.Form): + """ + DynamicEmailTest + """ + + to_email = forms.EmailField(label="To email", required=True) + + +class MailTemplateForm(ModelForm): + """ + MailTemplateForm + """ + + class Meta: + model = HorillaMailTemplate + fields = "__all__" + widgets = { + "body": forms.Textarea( + attrs={"data-summernote": "", "style": "display:none;"} + ), + } + + def get_template_language(self): + mail_data = { + "Receiver|Full name": "instance.get_full_name", + "Sender|Full name": "self.get_full_name", + "Receiver|Recruitment": "instance.recruitment_id", + "Sender|Recruitment": "self.recruitment_id", + "Receiver|Company": "instance.get_company", + "Sender|Company": "self.get_company", + "Receiver|Job position": "instance.get_job_position", + "Sender|Job position": "self.get_job_position", + "Receiver|Email": "instance.get_mail", + "Sender|Email": "self.get_mail", + "Receiver|Employee Type": "instance.get_employee_type", + "Sender|Employee Type": "self.get_employee_type", + "Receiver|Work Type": "instance.get_work_type", + "Sender|Work Type": "self.get_work_type", + "Candidate|Full name": "instance.get_full_name", + "Candidate|Recruitment": "instance.recruitment_id", + "Candidate|Company": "instance.get_company", + "Candidate|Job position": "instance.get_job_position", + "Candidate|Email": "instance.get_email", + "Candidate|Interview Table": "instance.get_interview|safe", + } + return mail_data + + def get_employee_template_language(self): + mail_data = { + "Receiver|Full name": "instance.get_full_name", + "Sender|Full name": "self.get_full_name", + "Receiver|Recruitment": "instance.recruitment_id", + "Sender|Recruitment": "self.recruitment_id", + "Receiver|Company": "instance.get_company", + "Sender|Company": "self.get_company", + "Receiver|Job position": "instance.get_job_position", + "Sender|Job position": "self.get_job_position", + "Receiver|Email": "instance.get_mail", + "Sender|Email": "self.get_mail", + "Receiver|Employee Type": "instance.get_employee_type", + "Sender|Employee Type": "self.get_employee_type", + "Receiver|Work Type": "instance.get_work_type", + "Sender|Work Type": "self.get_work_type", + } + return mail_data + + +class MultipleApproveConditionForm(ModelForm): + CONDITION_CHOICE = [ + ("equal", _("Equal (==)")), + ("notequal", _("Not Equal (!=)")), + ("range", _("Range")), + ("lt", _("Less Than (<)")), + ("gt", _("Greater Than (>)")), + ("le", _("Less Than or Equal To (<=)")), + ("ge", _("Greater Than or Equal To (>=)")), + ("icontains", _("Contains")), + ] + + multi_approval_manager = forms.ChoiceField( + choices=[], + widget=forms.Select(attrs={"class": "oh-select oh-select-2 mb-2"}), + label=_("Approval Manager"), + required=True, + ) + condition_operator = forms.ChoiceField( + choices=CONDITION_CHOICE, + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2 mb-2", + "hx-trigger": "change", + "hx-target": "#conditionValueDiv", + "hx-get": "condition-value-fields", + }, + ), + ) + + class Meta: + model = MultipleApprovalCondition + fields = "__all__" + exclude = [ + "is_active", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + choices = [("reporting_manager_id", _("Reporting Manager"))] + [ + (employee.pk, str(employee)) for employee in Employee.objects.all() + ] + self.fields["multi_approval_manager"].choices = choices + + +class DynamicPaginationForm(ModelForm): + """ + Form for setting default pagination + """ + + class Meta: + model = DynamicPagination + fields = "__all__" + exclude = ("user_id",) + + +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), + ] + return result[0] if result else None + + +class AnnouncementForm(ModelForm): + """ + Announcement Form + """ + + employees = HorillaMultiSelectField( + queryset=Employee.objects.all(), + widget=HorillaMultiSelectWidget( + filter_route_name="employee-widget-filter", + filter_class=EmployeeFilter, + filter_instance_contex_name="f", + filter_template_path="employee_filters.html", + ), + label="Employees", + ) + + class Meta: + """ + Meta class for additional options + """ + + model = Announcement + fields = "__all__" + exclude = ["is_active"] + widgets = { + "description": forms.Textarea(attrs={"data-summernote": ""}), + } + + def clean_description(self): + description = self.cleaned_data.get("description", "").strip() + # Remove HTML tags and check if there's meaningful content + text_content = strip_tags(description).strip() + if not text_content: # Checks if the field is empty after stripping HTML + raise forms.ValidationError("Description is required.") + return description + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["attachments"] = MultipleFileField(label=_("Attachments")) + self.fields["attachments"].required = False + self.fields["description"].required = False + self.fields["disable_comments"].widget.attrs.update( + {"hx-on:click": "togglePublicComments()"} + ) + + def save(self, commit: bool = ...) -> Any: + attachement = [] + multiple_attachment_ids = [] + attachements = None + if self.files.getlist("attachments"): + attachements = self.files.getlist("attachments") + self.instance.attachement = attachements[0] + multiple_attachment_ids = [] + + for attachement in attachements: + file_instance = Attachment() + file_instance.file = attachement + file_instance.save() + multiple_attachment_ids.append(file_instance.pk) + instance = super().save(commit) + if commit: + instance.attachements.add(*multiple_attachment_ids) + return instance, multiple_attachment_ids + + def as_p(self, *args, **kwargs): + context = {"form": self} + return render_to_string("announcement/as_p.html", context) + + def clean(self): + cleaned_data = super().clean() + + # Remove 'employees' field error if it's handled manually + if isinstance(self.fields["employees"], HorillaMultiSelectField): + self.errors.pop("employees", None) + employee_data = self.fields["employees"].queryset.filter( + id__in=self.data.getlist("employees") + ) + cleaned_data["employees"] = employee_data + + # Get submitted M2M values + employees_selected = cleaned_data.get("employees") + departments_selected = self.cleaned_data.get("department") + job_positions_selected = self.cleaned_data.get("job_position") + + # Check if none of the three are selected + if ( + not employees_selected + and not departments_selected + and not job_positions_selected + ): + raise forms.ValidationError( + _( + "You must select at least one of: Employees, Department, or Job Position." + ) + ) + + return cleaned_data + + +class AnnouncementCommentForm(ModelForm): + """ + Announcement comment form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = AnnouncementComment + fields = ["comment"] + + +class AnnouncementExpireForm(ModelForm): + """ + Announcement Expire form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = AnnouncementExpire + fields = ("days",) + + +class DriverForm(forms.ModelForm): + """ + DriverForm + """ + + class Meta: + model = DriverViewed + fields = "__all__" + + +UserModel = get_user_model() + + +class PassWordResetForm(forms.Form): + email = forms.CharField() + + def send_mail( + self, + subject_template_name, + email_template_name, + context, + from_email, + to_email, + html_email_template_name=None, + ): + """ + Send a django.core.mail.EmailMultiAlternatives to `to_email`. + """ + subject = loader.render_to_string(subject_template_name, context) + # Email subject *must not* contain newlines + subject = "".join(subject.splitlines()) + body = loader.render_to_string(email_template_name, context) + + email_message = EmailMultiAlternatives(subject, body, from_email, [to_email]) + if html_email_template_name is not None: + html_email = loader.render_to_string(html_email_template_name, context) + email_message.attach_alternative(html_email, "text/html") + + email_message.send() + + def get_users(self, email): + """ + Given an email, return matching user(s) who should receive a reset. + + This allows subclasses to more easily customize the default policies + that prevent inactive users and users with unusable passwords from + resetting their password. + """ + email_field_name = UserModel.get_email_field_name() + active_users = UserModel._default_manager.filter( + **{ + "%s__iexact" % email_field_name: email, + "is_active": True, + } + ) + return ( + u + for u in active_users + if u.has_usable_password() + and _unicode_ci_compare(email, getattr(u, email_field_name)) + ) + + def save( + self, + domain_override=None, + subject_template_name="registration/password_reset_subject.txt", + email_template_name="registration/password_reset_email.html", + use_https=False, + token_generator=default_token_generator, + from_email=None, + request=None, + html_email_template_name=None, + extra_email_context=None, + ): + """ + Generate a one-use only link for resetting password and send it to the + user. + """ + username = self.cleaned_data["email"] + user = User.objects.get(username=username) + employee = user.employee_get + email = employee.email + work_mail = None + try: + work_mail = employee.employee_work_info.email + except Exception as e: + pass + if work_mail: + email = work_mail + + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + if email: + token = token_generator.make_token(user) + context = { + "email": email, + "domain": domain, + "site_name": site_name, + "uid": urlsafe_base64_encode(force_bytes(user.pk)), + "user": user, + "token": token, + "protocol": "https" if use_https else "http", + **(extra_email_context or {}), + } + self.send_mail( + subject_template_name, + email_template_name, + context, + from_email, + email, + html_email_template_name=html_email_template_name, + ) + + +def validate_ip_or_cidr(value): + try: + ipaddress.ip_address(value) + except ValueError: + try: + ipaddress.ip_network(value, strict=False) + except ValueError: + raise ValidationError( + f"{value} is not a valid IP address or CIDR notation." + ) + + +class AttendanceAllowedIPForm(forms.ModelForm): + ip_addresses = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3, "class": "form-control w-100"}), + label="Allowed IP Addresses or Network Prefixes", + help_text="Enter multiple IP addresses or network prefixes, separated by commas.", + ) + + class Meta: + model = AttendanceAllowedIP + fields = ["ip_addresses"] + + def clean_ip_addresses(self): + ip_addresses = self.cleaned_data.get("ip_addresses", "").strip().split("\n") + cleaned_ips = [] + for ip in ip_addresses: + ip = ip.strip().split(", ") + if ip: + for ip_addr in ip: + validate_ip_or_cidr(ip_addr) + cleaned_ips.append(ip_addr) + return cleaned_ips + + def save(self, commit=True): + instance = super().save(commit=False) + if instance.pk: + existing_ips = set(instance.additional_data.get("allowed_ips", [])) + new_ips = set(self.cleaned_data["ip_addresses"]) + merged_ips = list(existing_ips.union(new_ips)) + instance.additional_data["allowed_ips"] = merged_ips + else: + instance.additional_data = { + "allowed_ips": self.cleaned_data["ip_addresses"] + } + + if commit: + instance.save() + + return instance + + +class AttendanceAllowedIPUpdateForm(ModelForm): + ip_address = forms.CharField(max_length=30, label="IP Address") + + class Meta: + model = AttendanceAllowedIP + fields = ["ip_address"] + + def validate_ip_address(self, value): + try: + validate_ipv46_address(value) + except ValidationError: + raise ValidationError("Enter a valid IPv4 or IPv6 address.") + return value + + def clean(self): + cleaned_data = super().clean() + + for field_name, value in self.data.items(): + cleaned_data[field_name] = self.validate_ip_address(value) + + return cleaned_data + + +class TrackLateComeEarlyOutForm(ModelForm): + class Meta: + model = TrackLateComeEarlyOut + fields = ["is_enable"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["is_enable"].widget.attrs.update( + { + "hx-post": "/attendance/enable-disable-tracking-late-come-early-out", + "hx-target": "this", + "hx-trigger": "change", + } + ) + + +class HolidayForm(ModelForm): + """ + Form for creating or updating a holiday. + + This form allows users to create or update holiday data by specifying details such as + the start date and end date. + """ + + def clean_end_date(self): + start_date = self.cleaned_data.get("start_date") + end_date = self.cleaned_data.get("end_date") + + if start_date and end_date and end_date < start_date: + raise ValidationError( + _("End date should not be earlier than the start date.") + ) + + return end_date + + class Meta: + """ + Meta class for additional options + """ + + model = Holidays + fields = "__all__" + exclude = ["is_active"] + labels = { + "name": _("Name"), + } + + def __init__(self, *args, **kwargs): + super(HolidayForm, self).__init__(*args, **kwargs) + self.fields["name"].widget.attrs["autocomplete"] = "name" + + +class HolidaysColumnExportForm(forms.Form): + """ + Form for selecting columns to export in holiday data. + """ + + selected_fields = forms.MultipleChoiceField( + choices=[], + widget=forms.CheckboxSelectMultiple, + initial=[], + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Use a single model instance for dynamic verbose names + model_instance = Holidays() + meta = model_instance._meta + + field_choices = [ + (field.name, meta.get_field(field.name).verbose_name) + for field in meta.fields + if field.name not in excluded_fields + ] + + self.fields["selected_fields"].choices = field_choices + self.fields["selected_fields"].initial = [ + "name", + "start_date", + "end_date", + "recurring", + ] + + +class CompanyLeaveForm(ModelForm): + """ + Form for managing company leave data. + + This form allows users to manage company leave data by including all fields from + the CompanyLeaves model except for is_active. + + Attributes: + - Meta: Inner class defining metadata options. + - model: The model associated with the form (CompanyLeaves). + - fields: A special value indicating all fields should be included in the form. + - exclude: A list of fields to exclude from the form (is_active). + """ + + class Meta: + """ + Meta class for additional options + """ + + model = CompanyLeaves + fields = "__all__" + exclude = ["is_active"] + + def __init__(self, *args, **kwargs): + """ + Custom initialization to configure the 'based_on' field. + """ + super().__init__(*args, **kwargs) + choices = [("", "All")] + list(self.fields["based_on_week"].choices[1:]) + self.fields["based_on_week"].choices = choices + self.fields["based_on_week"].widget.option_template_name = ( + "horilla_widgets/select_option.html" + ) + + +class PenaltyAccountForm(ModelForm): + """ + PenaltyAccountForm + """ + + class Meta: + model = PenaltyAccounts + fields = "__all__" + exclude = ["is_active"] + + def __init__(self, *args, **kwargs): + employee = kwargs.pop("employee", None) + super().__init__(*args, **kwargs) + if apps.is_installed("leave") and employee: + LeaveType = get_horilla_model_class(app_label="leave", model="leavetype") + available_leaves = employee.available_leave.all() + assigned_leave_types = LeaveType.objects.filter( + id__in=available_leaves.values_list("leave_type_id", flat=True) + ) + self.fields["leave_type_id"].queryset = assigned_leave_types diff --git a/base/horilla_company_manager.py b/base/horilla_company_manager.py new file mode 100644 index 0000000..68a4d69 --- /dev/null +++ b/base/horilla_company_manager.py @@ -0,0 +1,112 @@ +""" +horilla_company_manager.py +""" + +import logging +from typing import Coroutine, Sequence + +from django.db import models +from django.db.models.query import QuerySet + +from horilla.horilla_middlewares import _thread_locals +from horilla.signals import post_bulk_update, pre_bulk_update + +logger = logging.getLogger(__name__) +django_filter_update = QuerySet.update + + +def update(self, *args, **kwargs): + # pre_update signal + request = getattr(_thread_locals, "request", None) + self.request = request + pre_bulk_update.send(sender=self.model, queryset=self, args=args, kwargs=kwargs) + result = django_filter_update(self, *args, **kwargs) + # post_update signal + post_bulk_update.send(sender=self.model, queryset=self, args=args, kwargs=kwargs) + + return result + + +setattr(QuerySet, "update", update) + + +class HorillaCompanyManager(models.Manager): + """ + HorillaCompanyManager + """ + + def __init__(self, related_company_field=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.related_company_field = related_company_field + self.check_fields = [ + "employee_id", + "requested_employee_id", + ] + + def get_queryset(self): + """ + get_queryset method + """ + + queryset = super().get_queryset() + request = getattr(_thread_locals, "request", None) + selected_company = None + if request is not None: + selected_company = request.session.get("selected_company") + try: + queryset = ( + queryset.filter(self.model.company_filter) + if selected_company != "all" and selected_company + else queryset + ) + except Exception as e: + logger.error(e) + try: + has_duplicates = queryset.count() != queryset.distinct().count() + if has_duplicates: + queryset = queryset.distinct() + except: + pass + return queryset + + def all(self): + """ + Override the all() method + """ + queryset = [] + try: + queryset = self.get_queryset() + if queryset.exists(): + try: + model_name = queryset.model._meta.model_name + if model_name == "employee": + request = getattr(_thread_locals, "request", None) + if not getattr(request, "is_filtering", None): + queryset = queryset.filter(is_active=True) + else: + for field in queryset.model._meta.fields: + if isinstance(field, models.ForeignKey): + if field.name in self.check_fields: + related_model_is_active_filter = { + f"{field.name}__is_active": True + } + queryset = queryset.filter( + **related_model_is_active_filter + ) + except: + pass + except: + pass + return queryset + + def filter(self, *args, **kwargs): + queryset = super().filter(*args, **kwargs) + setattr(_thread_locals, "queryset_filter", queryset) + return queryset + + def entire(self): + """ + Fetch all datas from a model without applying any company filter. + """ + queryset = super().get_queryset() + return queryset # No filtering applied diff --git a/base/methods.py b/base/methods.py new file mode 100644 index 0000000..688ceaf --- /dev/null +++ b/base/methods.py @@ -0,0 +1,1154 @@ +import ast +import calendar +import json +import os +import random +from datetime import date, datetime, time, timedelta + +import pandas as pd +import pdfkit +from django.apps import apps +from django.conf import settings +from django.contrib.auth.models import Group +from django.contrib.staticfiles import finders +from django.core.exceptions import ObjectDoesNotExist +from django.core.paginator import Paginator +from django.db import models +from django.db.models import ForeignKey, ManyToManyField, OneToOneField, Q +from django.db.models.functions import Lower +from django.forms.models import ModelChoiceField +from django.http import HttpResponse +from django.template.loader import render_to_string +from django.utils.translation import gettext as _ + +from base.models import Company, CompanyLeaves, DynamicPagination, Holidays +from employee.models import Employee, EmployeeWorkInformation +from horilla.horilla_apps import NESTED_SUBORDINATE_VISIBILITY +from horilla.horilla_middlewares import _thread_locals +from horilla.horilla_settings import HORILLA_DATE_FORMATS, HORILLA_TIME_FORMATS + + +def users_count(self): + """ + Restrict Group users_count to selected company context + """ + return Employee.objects.filter(employee_user_id__in=self.user_set.all()).count() + + +Group.add_to_class("users_count", property(users_count)) + + +# def filtersubordinates(request, queryset, perm=None, field="employee_id"): +# """ +# This method is used to filter out subordinates queryset element. +# """ +# user = request.user +# if user.has_perm(perm): +# return queryset + +# if not request: +# return queryset +# if NESTED_SUBORDINATE_VISIBILITY: +# current_managers = [ +# request.user.employee_get.id, +# ] +# all_subordinates = Q( +# **{ +# f"{field}__employee_work_info__reporting_manager_id__in": current_managers +# } +# ) + +# while True: +# sub_managers = queryset.filter( +# **{ +# f"{field}__employee_work_info__reporting_manager_id__in": current_managers +# } +# ).values_list(f"{field}__id", flat=True) +# if not sub_managers.exists(): +# break +# current_managers = sub_managers +# all_subordinates |= Q( +# **{ +# f"{field}__employee_work_info__reporting_manager_id__in": sub_managers +# } +# ) + +# return queryset.filter(all_subordinates) + +# manager = Employee.objects.filter(employee_user_id=user).first() + +# if field: +# filter_expression = f"{field}__employee_work_info__reporting_manager_id" +# queryset = queryset.filter(**{filter_expression: manager}) +# return queryset + +# queryset = queryset.filter( +# employee_id__employee_work_info__reporting_manager_id=manager +# ) +# return queryset + + +def filtersubordinates( + request, + queryset, + perm=None, + field="employee_id", + nested=NESTED_SUBORDINATE_VISIBILITY, +): + """ + Filters a queryset to include only the current user's subordinates. + Respects the user's permission: if the user has `perm`, returns full queryset. + + Args: + request: HttpRequest + queryset: Django queryset to filter + perm: permission codename string + field: ForeignKey field pointing to Employee (default "employee_id") + nested: if True, include all nested subordinates; else only direct subordinates + + Returns: + Filtered queryset + """ + user = request.user + + if perm and user.has_perm(perm): + return queryset # User has permission to view all + + if not hasattr(user, "employee_get") or user.employee_get is None: + return queryset.none() # No employee associated, return empty + + # Get subordinate employee IDs + sub_ids = get_subordinate_employee_ids(request, nested=nested) + + # Include own records explicitly + own_id = user.employee_get.id + + # Build filter + filter_ids = sub_ids + [own_id] if sub_ids else [own_id] + + # Return filtered queryset + return queryset.filter(**{f"{field}__id__in": filter_ids}) + + +def filter_own_records(request, queryset, perm=None): + """ + This method is used to filter out subordinates queryset element. + """ + user = request.user + if user.has_perm(perm): + return queryset + queryset = queryset.filter(employee_id=request.user.employee_get) + return queryset + + +def filter_own_and_subordinate_recordes(request, queryset, perm=None): + """ + This method is used to filter out subordinates queryset along with own queryset element. + """ + user = request.user + if user.has_perm(perm): + return queryset + queryset = filter_own_records(request, queryset, perm) | filtersubordinates( + request, queryset, perm + ) + return queryset + + +def filtersubordinatesemployeemodel(request, queryset, perm=None): + """ + This method is used to filter out all subordinates in the entire reporting chain. + """ + user = request.user + if user.has_perm(perm): + return queryset + + if not request: + return queryset + + if NESTED_SUBORDINATE_VISIBILITY: + # Initialize the set of subordinates with the current manager(s) + current_managers = [ + request.user.employee_get.id, + ] + all_subordinates = Q( + employee_work_info__reporting_manager_id__in=current_managers + ) + + # Iteratively find subordinates in the chain + while True: + sub_managers = queryset.filter( + employee_work_info__reporting_manager_id__in=current_managers + ).values_list("id", flat=True) + + if not sub_managers.exists(): + break + + current_managers = sub_managers + all_subordinates |= Q( + employee_work_info__reporting_manager_id__in=sub_managers + ) + + # Apply the filter to the queryset + return queryset.filter(all_subordinates).distinct() + + manager = Employee.objects.filter(employee_user_id=user).first() + queryset = queryset.filter(employee_work_info__reporting_manager_id=manager) + return queryset + + +def is_reportingmanager(request): + """ + This method is used to check weather the employee is reporting manager or not. + """ + try: + user = request.user + return user.employee_get.reporting_manager.all().exists() + except: + return False + + +# def choosesubordinates( +# request, +# form, +# perm, +# ): +# user = request.user +# if user.has_perm(perm): +# return form +# manager = Employee.objects.filter(employee_user_id=user).first() +# queryset = Employee.objects.filter(employee_work_info__reporting_manager_id=manager) +# form.fields["employee_id"].queryset = queryset +# return form + + +def choosesubordinates(request, form, perm): + """ + Dynamically set subordinate choices for employee field based on permissions + and nested subordinate visibility. + """ + user = request.user + if user.has_perm(perm): + return form + manager = Employee.objects.filter(employee_user_id=user).first() + if not manager: + return form + + # Start with direct subordinates + current_managers = [manager.id] + all_subordinates = Q(employee_work_info__reporting_manager_id__in=current_managers) + + if NESTED_SUBORDINATE_VISIBILITY: + # Recursively find all subordinates in the chain + while True: + sub_managers = Employee.objects.filter( + employee_work_info__reporting_manager_id__in=current_managers + ).values_list("id", flat=True) + + if not sub_managers.exists(): + break + + current_managers = sub_managers + all_subordinates |= Q( + employee_work_info__reporting_manager_id__in=sub_managers + ) + + queryset = Employee.objects.filter(all_subordinates).distinct() + + # Assign to form field + if "employee_id" in form.fields: + form.fields["employee_id"].queryset = queryset + + return form + + +def get_subordinate_employee_ids(request, nested=NESTED_SUBORDINATE_VISIBILITY): + """ + Returns a list of subordinate Employee IDs under the current user. + + If nested=True, includes all subordinates recursively across the reporting hierarchy. + If nested=False, includes only direct subordinates. + """ + user = request.user + if not hasattr(user, "employee_get"): + return [] + + manager_id = user.employee_get.id + + if nested: + # Recursive approach for all levels + current_managers = [manager_id] + all_sub_ids = set() + + while current_managers: + sub_ids = list( + Employee.objects.filter( + employee_work_info__reporting_manager_id__in=current_managers + ).values_list("id", flat=True) + ) + if not sub_ids: + break + all_sub_ids.update(sub_ids) + current_managers = sub_ids + + return list(all_sub_ids) + else: + # Only direct subordinates + direct_sub_ids = list( + Employee.objects.filter( + employee_work_info__reporting_manager_id=manager_id + ).values_list("id", flat=True) + ) + return direct_sub_ids + + +def choosesubordinatesemployeemodel(request, form, perm): + user = request.user + if user.has_perm(perm): + return form + manager = Employee.objects.filter(employee_user_id=user).first() + queryset = Employee.objects.filter(employee_work_info__reporting_manager_id=manager) + + form.fields["employee_id"].queryset = queryset + return form + + +orderingList = [ + { + "id": "", + "field": "", + "ordering": "", + } +] + + +def sortby(request, queryset, key): + """ + This method is used to sort query set by asc or desc + """ + global orderingList + id = request.user.id + # here will create dictionary object to the global orderingList if not exists, + # if exists then method will switch corresponding object ordering. + filtered_list = [x for x in orderingList if x["id"] == id] + ordering = filtered_list[0] if filtered_list else None + if ordering is None: + ordering = { + "id": id, + "field": None, + "ordering": "-", + } + orderingList.append(ordering) + sortby = request.GET.get(key) + sort_count = request.GET.getlist(key).count(sortby) + order = None + if sortby is not None and sortby != "": + + field_parts = sortby.split("__") + + model_meta = queryset.model._meta + + # here will update the orderingList + ordering["field"] = sortby + if sort_count % 2 == 0: + ordering["ordering"] = "-" + order = sortby + else: + ordering["ordering"] = "" + order = f"-{sortby}" + + for part in field_parts: + field = model_meta.get_field(part) + if isinstance(field, models.ForeignKey): + model_meta = field.related_model._meta + else: + if isinstance(field, models.CharField): + queryset = queryset.annotate(lower_title=Lower(sortby)) + queryset = queryset.order_by(f"{ordering['ordering']}lower_title") + else: + queryset = queryset.order_by(f'{ordering["ordering"]}{sortby}') + + orderingList = [item for item in orderingList if item["id"] != id] + orderingList.append(ordering) + setattr(request, "sort_option", {}) + request.sort_option["order"] = order + + return queryset + + +def random_color_generator(): + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + if r == g or g == b or b == r: + random_color_generator() + return f"rgba({r}, {g}, {b} , 0.7)" + + +# color_palette=[] +# Function to generate distinct colors for each object +def generate_colors(num_colors): + # Define a color palette with distinct colors + color_palette = [ + "rgba(255, 99, 132, 1)", # Red + "rgba(54, 162, 235, 1)", # Blue + "rgba(255, 206, 86, 1)", # Yellow + "rgba(75, 192, 192, 1)", # Green + "rgba(153, 102, 255, 1)", # Purple + "rgba(255, 159, 64, 1)", # Orange + ] + + if num_colors > len(color_palette): + for i in range(num_colors - len(color_palette)): + color_palette.append(random_color_generator()) + + colors = [] + for i in range(num_colors): + # color=random_color_generator() + colors.append(color_palette[i % len(color_palette)]) + + return colors + + +def get_key_instances(model, data_dict): + # Get all the models in the Django project + all_models = apps.get_models() + + # Initialize a list to store related models include the function argument model as foreignkey + related_models = [] + + # Iterate through all models + for other_model in all_models: + # Iterate through fields of the model + for field in other_model._meta.fields: + # Check if the field is a ForeignKey and related to the function argument model + if isinstance(field, ForeignKey) and field.related_model == model: + related_models.append(other_model) + break + + # Iterate through related models to filter instances + for related_model in related_models: + # Get all fields of the related model + related_model_fields = related_model._meta.get_fields() + + # Iterate through fields to find ForeignKey fields + for field in related_model_fields: + if isinstance(field, ForeignKey): + # Get the related name and field name + related_name = field.related_query_name() + field_name = field.name + + # Check if the related name exists in data_dict + if related_name in data_dict: + # Get the related_id from data_dict + related_id_list = data_dict[related_name] + related_id = int(related_id_list[0]) + + # Filter instances based on the field and related_id + filtered_instance = related_model.objects.filter( + **{field_name: related_id} + ).first() + + # Store the filtered instance back in data_dict + data_dict[related_name] = [str(filtered_instance)] + + # Get all the fields in the argument model + model_fields = model._meta.get_fields() + foreign_key_field_names = [ + field.name + for field in model_fields + if isinstance(field, ForeignKey or OneToOneField) + ] + # Create a list of field names that are present in data_dict + present_foreign_key_field_names = [ + key for key in foreign_key_field_names if key in data_dict + ] + + for field_name in present_foreign_key_field_names: + try: + # Get the list of integer values from data_dict for the field + field_values = [int(value) for value in data_dict[field_name]] + + # Get the related model of the ForeignKey field + related_model = model._meta.get_field(field_name).remote_field.model + + # Get the instances of the related model using the field values + related_instances = related_model.objects.filter(id__in=field_values) + + # Create a list of string representations of the instances + related_strings = [str(instance) for instance in related_instances] + + # Update data_dict with the list of string representations + data_dict[field_name] = related_strings + except (ObjectDoesNotExist, ValueError): + pass + + # Create a list of field names that are ManyToManyField + many_to_many_field_names = [ + field.name for field in model_fields if isinstance(field, ManyToManyField) + ] + # Create a list of field names that are present in data_dict for ManyToManyFields + present_many_to_many_field_names = [ + key for key in many_to_many_field_names if key in data_dict + ] + + for field_name in present_many_to_many_field_names: + try: + # Get the related model of the ManyToMany field + related_model = model._meta.get_field(field_name).remote_field.model + # Get a list of integer values from data_dict for the field + field_values = [int(value) for value in data_dict[field_name]] + + # Filter instances of the related model based on the field values + related_instances = related_model.objects.filter(id__in=field_values) + + # Update data_dict with the string representations of related instances + data_dict[field_name] = [str(instance) for instance in related_instances] + except (ObjectDoesNotExist, ValueError): + pass + + nested_fields = [ + key + for key in data_dict + if "__" in key and not key.endswith("gte") and not key.endswith("lte") + ] + for key in nested_fields: + field_names = key.split("__") + field_values = data_dict[key] + if ( + field_values != ["unknown"] + and field_values != ["true"] + and field_values != ["false"] + ): + nested_instance = get_nested_instances(model, field_names, field_values) + if nested_instance is not None: + data_dict[key] = nested_instance + + if "id" in data_dict: + id = data_dict["id"][0] + object = model.objects.filter(id=id).first() + object = str(object) + del data_dict["id"] + data_dict["Object"] = [object] + keys_to_remove = [ + key + for key, value in data_dict.items() + if value == ["unknown"] + or key + in [ + "sortby", + "orderby", + "view", + "page", + "group_by", + "target", + "rpage", + "instances_ids", + "asset_list", + "vpage", + "opage", + "click_id", + "csrfmiddlewaretoken", + "assign_sortby", + "request_sortby", + "asset_under", + ] + or "dynamic_page" in key + ] + if not "search" in data_dict: + if "search_field" in data_dict: + del data_dict["search_field"] + + for key in keys_to_remove: + del data_dict[key] + return data_dict + + +def get_nested_instances(model, field_names, field_values): + try: + related_model = model + for field_name in field_names: + try: + related_field = related_model._meta.get_field(field_name) + except: + pass + try: + related_model = related_field.remote_field.model + except: + pass + object_ids = [int(value) for value in field_values if value != "not_set"] + related_instances = related_model.objects.filter(id__in=object_ids) + result = [str(instance) for instance in related_instances] + if "not_set" in field_values: + result.insert(0, "not_set") + return result + except (ObjectDoesNotExist, ValueError): + return None + + +def closest_numbers(numbers: list, input_number: int) -> tuple: + """ + This method is used to find previous and next of numbers + """ + previous_number = input_number + next_number = input_number + try: + index = numbers.index(input_number) + if index > 0: + previous_number = numbers[index - 1] + else: + previous_number = numbers[-1] + if index + 1 == len(numbers): + next_number = numbers[0] + elif index < len(numbers): + next_number = numbers[index + 1] + else: + next_number = numbers[0] + except: + pass + return (previous_number, next_number) + + +def format_export_value(value, employee): + work_info = EmployeeWorkInformation.objects.filter(employee_id=employee).first() + time_format = ( + work_info.company_id.time_format + if work_info and work_info.company_id + else "HH:mm" + ) + date_format = ( + work_info.company_id.date_format + if work_info and work_info.company_id + else "MMM. D, YYYY" + ) + + if isinstance(value, time): + # Convert the string to a datetime.time object + check_in_time = datetime.strptime(str(value).split(".")[0], "%H:%M:%S").time() + + # Print the formatted time for each format + for format_name, format_string in HORILLA_TIME_FORMATS.items(): + if format_name == time_format: + value = check_in_time.strftime(format_string) + + elif type(value) == date: + # Convert the string to a datetime.date object + start_date = datetime.strptime(str(value), "%Y-%m-%d").date() + # Print the formatted date for each format + for format_name, format_string in HORILLA_DATE_FORMATS.items(): + if format_name == date_format: + value = start_date.strftime(format_string) + + elif isinstance(value, datetime): + value = str(value) + + return value + + +def export_data(request, model, form_class, filter_class, file_name, perm=None): + fields_mapping = { + "male": _("Male"), + "female": _("Female"), + "other": _("Other"), + "draft": _("Draft"), + "active": _("Active"), + "expired": _("Expired"), + "terminated": _("Terminated"), + "weekly": _("Weekly"), + "monthly": _("Monthly"), + "after": _("After"), + "semi_monthly": _("Semi-Monthly"), + "hourly": _("Hourly"), + "daily": _("Daily"), + "monthly": _("Monthly"), + "full_day": _("Full Day"), + "first_half": _("First Half"), + "second_half": _("Second Half"), + "requested": _("Requested"), + "approved": _("Approved"), + "cancelled": _("Cancelled"), + "rejected": _("Rejected"), + "cancelled_and_rejected": _("Cancelled & Rejected"), + "late_come": _("Late Come"), + "early_out": _("Early Out"), + } + employee = request.user.employee_get + + selected_columns = [] + today_date = date.today().strftime("%Y-%m-%d") + file_name = f"{file_name}_{today_date}.xlsx" + data_export = {} + + form = form_class() + model_fields = model._meta.get_fields() + export_objects = filter_class(request.GET).qs + if perm: + export_objects = filtersubordinates(request, export_objects, perm) + selected_fields = request.GET.getlist("selected_fields") + + if not selected_fields: + selected_fields = form.fields["selected_fields"].initial + ids = request.GET.get("ids") + id_list = json.loads(ids) + export_objects = model.objects.filter(id__in=id_list) + + for field in form.fields["selected_fields"].choices: + value = field[0] + key = field[1] + if value in selected_fields: + selected_columns.append((value, key)) + + for field_name, verbose_name in selected_columns: + if field_name in selected_fields: + data_export[verbose_name] = [] + for obj in export_objects: + value = obj + nested_attributes = field_name.split("__") + for attr in nested_attributes: + value = getattr(value, attr, None) + if value is None: + break + if value is True: + value = _("Yes") + elif value is False: + value = _("No") + if value in fields_mapping: + value = fields_mapping[value] + if value == "None": + value = " " + if field_name == "month": + value = _(value.title()) + + # Check if the type of 'value' is time + value = format_export_value(value, employee) + data_export[verbose_name].append(value) + + data_frame = pd.DataFrame(data=data_export) + styled_data_frame = data_frame.style.applymap( + lambda x: "text-align: center", subset=pd.IndexSlice[:, :] + ) + + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = f'attachment; filename="{file_name}"' + + writer = pd.ExcelWriter(response, engine="xlsxwriter") + styled_data_frame.to_excel(writer, index=False, sheet_name="Sheet1") + worksheet = writer.sheets["Sheet1"] + worksheet.set_column("A:Z", 18) + writer.close() + + return response + + +def reload_queryset(fields): + """ + Reloads querysets in the form based on active filters and selected company. + """ + request = getattr(_thread_locals, "request", None) + selected_company = request.session.get("selected_company") if request else None + + recruitment_installed = apps.is_installed("recruitment") + model_filters = { + "Employee": {"is_active": True}, + "Candidate": {"is_active": True} if recruitment_installed else None, + } + + for field in fields.values(): + if not isinstance(field, ModelChoiceField): + continue + + model = field.queryset.model + model_name = model.__name__ + + if model_name == "Company" and selected_company and selected_company != "all": + field.queryset = model.objects.filter(id=selected_company) + elif (filters := model_filters.get(model_name)) is not None: + field.queryset = model.objects.filter(**filters) + else: + field.queryset = model.objects.all() + + return fields + + +def check_manager(employee, instance): + + try: + if isinstance(instance, Employee): + return instance.employee_work_info.reporting_manager_id == employee + return employee == instance.employee_id.employee_work_info.reporting_manager_id + except: + return False + + +def check_owner(employee, instance): + try: + if isinstance(instance, Employee): + return employee == instance + return employee == instance.employee_id + except: + return False + + +def link_callback(uri, rel): + """ + Convert HTML URIs to absolute system paths so xhtml2pdf can access those + resources + """ + if not uri.startswith("/static"): + return uri + uri = "payroll/fonts/Poppins_Regular.ttf" + result = finders.find(uri) + if result: + if not isinstance(result, (list, tuple)): + result = [result] + + result = list(os.path.realpath(path) for path in result) + path = result[0] + + else: + sUrl = settings.STATIC_URL + sRoot = settings.STATIC_ROOT + mUrl = settings.MEDIA_URL + mRoot = settings.MEDIA_ROOT + + if uri.startswith(sUrl): + path = os.path.join(sRoot, uri.replace(sUrl, "")) + else: + return uri + + if os.name == "nt": + return uri + + if not os.path.isfile(path): + raise RuntimeError("media URI must start with %s or %s" % (sUrl, mUrl)) + return path + + +# def generate_pdf(template_path, context, path=True, title=None, html=True): +# template_path = template_path +# context_data = context +# title = ( +# f"""{context_data.get("employee")}'s payslip for {context_data.get("range")}.pdf""" +# if not title +# else title +# ) +# response = HttpResponse(content_type="application/pdf") +# response["Content-Disposition"] = f"attachment; filename={title}" + +# if html: +# html = template_path +# else: +# template = get_template(template_path) +# html = template.render(context_data) + +# pisa_status = pisa.CreatePDF( +# html.encode("utf-8"), +# dest=response, +# link_callback=link_callback, +# ) + +# if pisa_status.err: +# return HttpResponse("We had some errors
" + html + "
") + +# return response + + +def generate_pdf(template_path, context, path=True, title=None, html=True): + title = "Document" if not title else title + + if html: + html = template_path + else: + html = render_to_string(template_path, context) + + response = template_pdf(template=html, html=True, filename=title) + + return response + + +def get_pagination(): + from horilla.horilla_middlewares import _thread_locals + + request = getattr(_thread_locals, "request", None) + user = request.user + page = DynamicPagination.objects.filter(user_id=user).first() + count = 20 + if page: + count = page.pagination + return count + + +def paginator_qry(queryset, page_number): + """ + Common paginator method + """ + paginator = Paginator(queryset, get_pagination()) + queryset = paginator.get_page(page_number) + return queryset + + +def is_holiday(date): + """ + Check if the given date is a holiday. + Args: + date (datetime.date): The date to check. + Returns: + Holidays or bool: The Holidays object if the date is a holiday, otherwise False. + """ + # Get holidays that either match the exact date range or are recurring + holiday = Holidays.objects.filter( + Q(start_date__lte=date, end_date__gte=date) + | Q(recurring=True, start_date__month=date.month, start_date__day=date.day) + ).first() + return holiday if holiday else False + + +def is_company_leave(input_date): + """ + Check if the given date is a company leave. + Args: + input_date (datetime.date): The date to check. + Returns: + CompanyLeaves or bool: The CompanyLeaves object if the date is a company leave, otherwise False. + """ + # Calculate the week number within the month (0-4) and weekday (0 for Monday to 6 for Sunday) + first_day_of_month = input_date.replace(day=1) + adjusted_day = ( + input_date.day + first_day_of_month.weekday() + ) # Adjust day based on first day of the month + # Calculate the week number (0-based) + date_week_no = (adjusted_day - 1) // 7 + # Get weekday (0 for Monday to 6 for Sunday) + date_week_day = input_date.weekday() + + # Query for company leaves that match the week number and weekday + company_leave = CompanyLeaves.objects.filter( + Q( + based_on_week=None, based_on_week_day=date_week_day + ) # Match week-independent leaves + | Q( + based_on_week=date_week_no, based_on_week_day=date_week_day + ) # Match specific week and weekday + ).first() + + return company_leave if company_leave else False + + +def get_date_range(start_date, end_date): + """ + Returns a list of all dates within a given date range. + + Args: + start_date (date): The start date of the range. + end_date (date): The end date of the range. + + Returns: + list: A list of date objects representing all dates within the range. + + Example: + start_date = date(2023, 1, 1) + end_date = date(2023, 1, 10) + date_range = get_date_range(start_date, end_date) + + """ + date_list = [] + delta = end_date - start_date + + for i in range(delta.days + 1): + current_date = start_date + timedelta(days=i) + date_list.append(current_date) + return date_list + + +def get_holiday_dates(range_start: date, range_end: date) -> list: + """ + :return: this functions returns a list of all holiday dates. + """ + pay_range_dates = get_date_range(start_date=range_start, end_date=range_end) + query = Q() + for check_date in pay_range_dates: + query |= Q(start_date__lte=check_date, end_date__gte=check_date) + holidays = Holidays.objects.filter(query) + holiday_dates = set([]) + for holiday in holidays: + holiday_dates = holiday_dates | ( + set( + get_date_range(start_date=holiday.start_date, end_date=holiday.end_date) + ) + ) + return list(set(holiday_dates)) + + +def get_company_leave_dates(year): + """ + :return: This function returns a list of all company leave dates + """ + company_leaves = CompanyLeaves.objects.all() + company_leave_dates = [] + for company_leave in company_leaves: + based_on_week = company_leave.based_on_week + based_on_week_day = company_leave.based_on_week_day + for month in range(1, 13): + if based_on_week is not None: + # Set Sunday as the first day of the week + calendar.setfirstweekday(6) + month_calendar = calendar.monthcalendar(year, month) + weeks = month_calendar[int(based_on_week)] + weekdays_in_weeks = [day for day in weeks if day != 0] + for day in weekdays_in_weeks: + leave_date = datetime.strptime( + f"{year}-{month:02}-{day:02}", "%Y-%m-%d" + ).date() + if ( + leave_date.weekday() == int(based_on_week_day) + and leave_date not in company_leave_dates + ): + company_leave_dates.append(leave_date) + else: + # Set Monday as the first day of the week + calendar.setfirstweekday(0) + month_calendar = calendar.monthcalendar(year, month) + for week in month_calendar: + if week[int(based_on_week_day)] != 0: + leave_date = datetime.strptime( + f"{year}-{month:02}-{week[int(based_on_week_day)]:02}", + "%Y-%m-%d", + ).date() + if leave_date not in company_leave_dates: + company_leave_dates.append(leave_date) + return company_leave_dates + + +def get_working_days(start_date, end_date): + """ + This method is used to calculate the total working days, total leave, worked days on that period + + Args: + start_date (_type_): the start date from the data needed + end_date (_type_): the end date till the date needed + """ + + holiday_dates = get_holiday_dates(start_date, end_date) + + # appending company/holiday leaves + # Note: Duplicate entry may exist + company_leave_dates = ( + list( + set( + get_company_leave_dates(start_date.year) + + get_company_leave_dates(end_date.year) + ) + ) + + holiday_dates + ) + + date_range = get_date_range(start_date, end_date) + + # making unique list of company/holiday leave dates then filtering + # the leave dates only between the start and end date + company_leave_dates = [ + date + for date in list(set(company_leave_dates)) + if start_date <= date <= end_date + ] + + working_days_between_ranges = list(set(date_range) - set(company_leave_dates)) + total_working_days = len(working_days_between_ranges) + + return { + # Total working days on that period + "total_working_days": total_working_days, + # All the working dates between the start and end date + "working_days_on": working_days_between_ranges, + # All the company/holiday leave dates between the range + "company_leave_dates": company_leave_dates, + } + + +def get_next_month_same_date(date_obj): + date_copy = date_obj + month = date_obj.month + 1 + year = date_obj.year + if month > 12: + month = 1 + year = year + 1 + day = date_copy.day + total_days_in_month = calendar.monthrange(year, month)[1] + day = min(day, total_days_in_month) + return date(day=day, month=month, year=year) + + +def get_subordinates(request): + """ + This method is used to filter out subordinates queryset element. + """ + user = request.user.employee_get + subordinates = Employee.objects.filter( + employee_work_info__reporting_manager_id=user + ) + return subordinates + + +def format_date(date_str): + # List of possible date formats to try + + for format_name, format_string in HORILLA_DATE_FORMATS.items(): + try: + return datetime.strptime(date_str, format_string).strftime("%Y-%m-%d") + except ValueError: + continue + raise ValueError(f"Invalid date format: {date_str}") + + +def eval_validate(value): + """ + Method to validate the dynamic value + """ + value = ast.literal_eval(value) + return value + + +def template_pdf(template, context={}, html=False, filename="payslip.pdf"): + """ + Generate a PDF file from an HTML template and context data. + + Args: + template_path (str): The path to the HTML template. + context (dict): The context data to render the template. + html (bool): If True, return raw HTML instead of a PDF. + + Returns: + HttpResponse: A response with the generated PDF file or raw HTML. + """ + try: + bootstrap_css = '' + html_content = f"{bootstrap_css}\n{template}" + + pdf_options = { + "page-size": "A4", + "margin-top": "10mm", + "margin-bottom": "10mm", + "margin-left": "10mm", + "margin-right": "10mm", + "encoding": "UTF-8", + "enable-local-file-access": None, + "dpi": 300, + "zoom": 1.3, + "footer-center": "[page]/[topage]", + } + + pdf = pdfkit.from_string(html_content, False, options=pdf_options) + + response = HttpResponse(pdf, content_type="application/pdf") + response["Content-Disposition"] = f"inline; filename={filename}" + return response + except Exception as e: + return HttpResponse(f"Error generating PDF: {str(e)}", status=500) + + +def generate_otp(): + """ + Function to generate a random 6-digit OTP (One-Time Password). + Returns: + str: A 6-digit random OTP as a string. + """ + return str(random.randint(100000, 999999)) diff --git a/base/middleware.py b/base/middleware.py new file mode 100644 index 0000000..11ba4a8 --- /dev/null +++ b/base/middleware.py @@ -0,0 +1,249 @@ +""" +middleware.py +""" + +from django.apps import apps +from django.contrib import messages +from django.contrib.auth import logout +from django.core.cache import cache +from django.db.models import Q +from django.shortcuts import redirect +from django.utils.translation import gettext_lazy as _ + +from base.backends import ConfiguredEmailBackend +from base.context_processors import AllCompany +from base.horilla_company_manager import HorillaCompanyManager +from base.models import Company, ShiftRequest, WorkTypeRequest +from employee.models import ( + DisciplinaryAction, + Employee, + EmployeeBankDetails, + EmployeeWorkInformation, +) +from horilla.horilla_apps import TWO_FACTORS_AUTHENTICATION +from horilla.horilla_settings import APPS +from horilla.methods import get_horilla_model_class +from horilla_documents.models import DocumentRequest + +CACHE_KEY = "horilla_company_models_cache_key" + + +class CompanyMiddleware: + """ + Middleware to handle company-specific filtering for models. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def _get_company_id(self, request): + """ + Retrieve the company ID from the request or session. + """ + if getattr(request, "user", False) and not request.user.is_anonymous: + try: + if com_id := request.session.get("selected_company", None): + return ( + Company.objects.filter(id=com_id).first() + if com_id != "all" + else None + ) + else: + return getattr( + request.user.employee_get.employee_work_info, "company_id", None + ) + except AttributeError: + pass + return None + + def _set_company_session(self, request, company_id): + """ + Set the company session data based on the company ID. + """ + try: + user = request.user.employee_get + except Exception: + logout(request) + messages.error( + request, + _("An employee related to this user's credentials does not exist."), + ) + return redirect("login") + user_company_id = getattr( + getattr(user, "employee_work_info", None), "company_id", None + ) + if company_id and request.session.get("selected_company") != "all": + if company_id == "all": + text = "All companies" + elif company_id == user_company_id: + text = "My Company" + else: + text = "Other Company" + + request.session["selected_company"] = str(company_id.id) + request.session["selected_company_instance"] = { + "company": company_id.company, + "icon": company_id.icon.url, + "text": text, + "id": company_id.id, + } + else: + request.session["selected_company"] = "all" + all_company = AllCompany() + request.session["selected_company_instance"] = { + "company": all_company.company, + "icon": all_company.icon.url, + "text": all_company.text, + "id": all_company.id, + } + + def _add_company_filter(self, model, company_id): + """ + Add company filter to the model if applicable. + """ + is_company_model = model in self._get_company_models() + company_field = getattr(model, "company_id", None) + is_horilla_manager = isinstance(model.objects, HorillaCompanyManager) + related_company_field = getattr(model.objects, "related_company_field", None) + + if is_company_model: + if company_field: + model.add_to_class("company_filter", Q(company_id=company_id)) + elif is_horilla_manager and related_company_field: + model.add_to_class( + "company_filter", Q(**{related_company_field: company_id}) + ) + else: + if company_field: + model.add_to_class( + "company_filter", + Q(company_id=company_id) | Q(company_id__isnull=True), + ) + elif is_horilla_manager and related_company_field: + model.add_to_class( + "company_filter", + Q(**{related_company_field: company_id}) + | Q(**{f"{related_company_field}__isnull": True}), + ) + + def _get_company_models(self): + """ + Retrieve the list of models that are company-specific. + """ + company_models = cache.get(CACHE_KEY) + + if company_models is None: + company_models = [ + Employee, + ShiftRequest, + WorkTypeRequest, + DocumentRequest, + DisciplinaryAction, + EmployeeBankDetails, + EmployeeWorkInformation, + ] + + app_model_mappings = { + "recruitment": ["recruitment", "candidate"], + "leave": [ + "leaverequest", + "restrictleave", + "availableleave", + "leaveallocationrequest", + "compensatoryleaverequest", + ], + "asset": ["assetassignment", "assetrequest"], + "attendance": [ + "attendance", + "attendanceactivity", + "attendanceovertime", + "workrecords", + ], + "payroll": [ + "contract", + "loanaccount", + "payslip", + "reimbursement", + ], + "helpdesk": ["ticket"], + "offboarding": ["offboarding"], + "pms": ["employeeobjective"], + } + + for app_label, models in app_model_mappings.items(): + if apps.is_installed(app_label): + company_models.extend( + [get_horilla_model_class(app_label, model) for model in models] + ) + + cache.set(CACHE_KEY, company_models) + + return company_models + + def __call__(self, request): + if getattr(request, "user", False) and not request.user.is_anonymous: + company_id = self._get_company_id(request) + self._set_company_session(request, company_id) + + app_models = [ + model for model in apps.get_models() if model._meta.app_label in APPS + ] + for model in app_models: + self._add_company_filter(model, company_id) + + response = self.get_response(request) + return response + + +class ForcePasswordChangeMiddleware: + """ + Middleware to force password change for new employees. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + excluded_paths = ["/change-password", "/login", "/logout"] + if request.path.rstrip("/") in excluded_paths: + return self.get_response(request) + + if hasattr(request, "user") and request.user.is_authenticated: + if getattr(request.user, "is_new_employee", True): + return redirect("change-password") + + return self.get_response(request) + + +class TwoFactorAuthMiddleware: + """ + Middleware to enforce two-factor authentication for specific users. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + excluded_paths = [ + "/change-password", + "/login", + "/logout", + "/two-factor", + "/send-otp", + ] + + if request.path.rstrip("/") in excluded_paths: + return self.get_response(request) + + if TWO_FACTORS_AUTHENTICATION: + try: + if ConfiguredEmailBackend().configuration is not None: + if hasattr(request, "user") and request.user.is_authenticated: + if not request.session.get("otp_code_verified", False): + return redirect("/two-factor") + else: + return self.get_response(request) + except Exception as e: + return self.get_response(request) + + return self.get_response(request) diff --git a/base/models.py b/base/models.py new file mode 100644 index 0000000..f481a68 --- /dev/null +++ b/base/models.py @@ -0,0 +1,1865 @@ +""" +models.py + +This module is used to register django models +""" + +import ipaddress +from datetime import date, datetime, timedelta +from typing import Iterable + +import django +from django.apps import apps +from django.contrib import messages +from django.contrib.auth.models import AbstractUser, User +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from base.horilla_company_manager import HorillaCompanyManager +from horilla import horilla_middlewares +from horilla.horilla_middlewares import _thread_locals +from horilla.models import HorillaModel, upload_path +from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog + +# Create your models here. +WEEKS = [ + ("0", _("First Week")), + ("1", _("Second Week")), + ("2", _("Third Week")), + ("3", _("Fourth Week")), + ("4", _("Fifth Week")), +] + + +WEEK_DAYS = [ + ("0", _("Monday")), + ("1", _("Tuesday")), + ("2", _("Wednesday")), + ("3", _("Thursday")), + ("4", _("Friday")), + ("5", _("Saturday")), + ("6", _("Sunday")), +] + + +def validate_time_format(value): + """ + this method is used to validate the format of duration like fields. + """ + if len(value) > 6: + raise ValidationError(_("Invalid format, it should be HH:MM format")) + try: + hour, minute = value.split(":") + hour = int(hour) + minute = int(minute) + if len(str(hour)) > 3 or minute not in range(60): + raise ValidationError(_("Invalid time, excepted HH:MM")) + except ValueError as e: + raise ValidationError(_("Invalid format, excepted HH:MM")) from e + + +def clear_messages(request): + storage = messages.get_messages(request) + for message in storage: + pass + + +class Company(HorillaModel): + """ + Company model + """ + + company = models.CharField(max_length=50, verbose_name=_("Name")) + hq = models.BooleanField(default=False) + address = models.TextField(max_length=255) + country = models.CharField(max_length=50) + state = models.CharField(max_length=50) + city = models.CharField(max_length=50) + zip = models.CharField(max_length=20) + icon = models.FileField( + upload_to=upload_path, + null=True, + ) + objects = models.Manager() + date_format = models.CharField(max_length=30, blank=True, null=True) + time_format = models.CharField(max_length=20, blank=True, null=True) + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Company") + verbose_name_plural = _("Companies") + unique_together = ["company", "address"] + app_label = "base" + + def __str__(self) -> str: + return str(self.company) + + +class Department(HorillaModel): + """ + Department model + """ + + department = models.CharField( + max_length=50, blank=False, verbose_name=_("Department") + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager() + + class Meta: + verbose_name = _("Department") + verbose_name_plural = _("Departments") + + def clean(self, *args, **kwargs): + super().clean(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + if request and request.POST: + company = request.POST.getlist("company_id", None) + department = request.POST.get("department", None) + if ( + Department.objects.filter( + company_id__id__in=company, department=department + ) + .exclude(id=self.id) + .exists() + ): + raise ValidationError("This department already exists in this company") + return + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.clean(*args, **kwargs) + return self + + def __str__(self): + return str(self.department) + + +class JobPosition(HorillaModel): + """ + JobPosition model + """ + + job_position = models.CharField( + max_length=50, blank=False, null=False, verbose_name=_("Job Position") + ) + department_id = models.ForeignKey( + Department, + on_delete=models.PROTECT, + related_name="job_position", + verbose_name=_("Department"), + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager("department_id__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Job Position") + verbose_name_plural = _("Job Positions") + + def __str__(self): + return str(self.job_position + " - (" + self.department_id.department) + ")" + + +class JobRole(HorillaModel): + """JobRole model""" + + job_position_id = models.ForeignKey( + JobPosition, on_delete=models.PROTECT, verbose_name=_("Job Position") + ) + job_role = models.CharField( + max_length=50, blank=False, null=True, verbose_name=_("Job Role") + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager("job_position_id__department_id__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Job Role") + verbose_name_plural = _("Job Roles") + unique_together = ("job_position_id", "job_role") + + def __str__(self): + return f"{self.job_role} - {self.job_position_id.job_position}" + + +class WorkType(HorillaModel): + """ + WorkType model + """ + + work_type = models.CharField(max_length=50, verbose_name=_("Work Type")) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager() + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Work Type") + verbose_name_plural = _("Work Types") + + def __str__(self) -> str: + return str(self.work_type) + + def clean(self, *args, **kwargs): + super().clean(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + if request and request.POST: + company = request.POST.getlist("company_id", None) + work_type = request.POST.get("work_type", None) + if ( + WorkType.objects.filter(company_id__id__in=company, work_type=work_type) + .exclude(id=self.id) + .exists() + ): + raise ValidationError("This work type already exists in this company") + return + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.clean(*args, **kwargs) + return self + + +class RotatingWorkType(HorillaModel): + """ + RotatingWorkType model + """ + + name = models.CharField(max_length=50) + work_type1 = models.ForeignKey( + WorkType, + on_delete=models.PROTECT, + related_name="work_type1", + verbose_name=_("Work Type 1"), + ) + work_type2 = models.ForeignKey( + WorkType, + on_delete=models.PROTECT, + related_name="work_type2", + verbose_name=_("Work Type 2"), + ) + employee_id = models.ManyToManyField( + "employee.Employee", + through="RotatingWorkTypeAssign", + verbose_name=_("Employee"), + ) + additional_data = models.JSONField( + default=dict, + blank=True, + null=True, + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Rotating Work Type") + verbose_name_plural = _("Rotating Work Types") + + def __str__(self) -> str: + return str(self.name) + + def clean(self): + if self.work_type1 == self.work_type2: + raise ValidationError(_("Select different work type continuously")) + + additional_work_types = ( + self.additional_data.get("additional_work_types", []) + if self.additional_data + else [] + ) + + if ( + additional_work_types + and str(self.work_type2.id) == additional_work_types[0] + ): + raise ValidationError(_("Select different work type continuously")) + + if ( + additional_work_types + and str(self.work_type1.id) == additional_work_types[-1] + ): + raise ValidationError(_("Select different work type continuously")) + + for i in range(len(additional_work_types) - 1): + if additional_work_types[i] and additional_work_types[i + 1]: + if additional_work_types[i] == additional_work_types[i + 1]: + raise ValidationError(_("Select different work type continuously")) + + def additional_work_types(self): + rotating_work_type = RotatingWorkType.objects.get(id=self.pk) + additional_data = rotating_work_type.additional_data + if additional_data: + additional_work_type_ids = additional_data.get("additional_work_types") + if additional_work_type_ids: + additional_work_types = WorkType.objects.filter( + id__in=additional_work_type_ids + ) + else: + additional_work_types = None + else: + additional_work_types = None + return additional_work_types + + +DAY_DATE = [(str(i), str(i)) for i in range(1, 32)] +DAY_DATE.append(("last", _("Last Day"))) +DAY = [ + ("monday", _("Monday")), + ("tuesday", _("Tuesday")), + ("wednesday", _("Wednesday")), + ("thursday", _("Thursday")), + ("friday", _("Friday")), + ("saturday", _("Saturday")), + ("sunday", _("Sunday")), +] +BASED_ON = [ + ("after", _("After")), + ("weekly", _("Weekend")), + ("monthly", _("Monthly")), +] + + +class RotatingWorkTypeAssign(HorillaModel): + """ + RotatingWorkTypeAssign model + """ + + employee_id = models.ForeignKey( + "employee.Employee", + on_delete=models.PROTECT, + null=True, + verbose_name=_("Employee"), + ) + rotating_work_type_id = models.ForeignKey( + RotatingWorkType, on_delete=models.PROTECT, verbose_name=_("Rotating Work Type") + ) + start_date = models.DateField( + default=django.utils.timezone.now, verbose_name=_("Start Date") + ) + next_change_date = models.DateField(null=True, verbose_name=_("Next Switch")) + current_work_type = models.ForeignKey( + WorkType, + null=True, + on_delete=models.PROTECT, + related_name="current_work_type", + verbose_name=_("Current Work Type"), + ) + next_work_type = models.ForeignKey( + WorkType, + null=True, + on_delete=models.PROTECT, + related_name="next_work_type", + verbose_name=_("Next Work Type"), + ) + based_on = models.CharField( + max_length=10, + choices=BASED_ON, + null=False, + blank=False, + verbose_name=_("Based On"), + ) + rotate_after_day = models.IntegerField( + default=7, verbose_name=_("Rotate After Day") + ) + rotate_every_weekend = models.CharField( + max_length=10, + default="monday", + choices=DAY, + blank=True, + null=True, + verbose_name=_("Rotate Every Weekend"), + ) + rotate_every = models.CharField( + max_length=10, + default="1", + choices=DAY_DATE, + verbose_name=_("Rotate Every Month"), + ) + additional_data = models.JSONField( + default=dict, + blank=True, + null=True, + ) + history = HorillaAuditLog( + related_name="history_set", + bases=[ + HorillaAuditInfo, + ], + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Rotating Work Type Assign") + verbose_name_plural = _("Rotating Work Type Assigns") + ordering = ["-next_change_date", "-employee_id__employee_first_name"] + + def clean(self): + if self.is_active and self.employee_id is not None: + # Check if any other active record with the same parent already exists + siblings = RotatingWorkTypeAssign.objects.filter( + is_active=True, employee_id=self.employee_id + ) + if siblings.exists() and siblings.first().id != self.id: + raise ValidationError(_("Only one active record allowed per employee")) + if self.start_date < django.utils.timezone.now().date(): + raise ValidationError(_("Date must be greater than or equal to today")) + + +class EmployeeType(HorillaModel): + """ + EmployeeType model + """ + + employee_type = models.CharField(max_length=50) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Employee Type") + verbose_name_plural = _("Employee Types") + + def __str__(self) -> str: + return str(self.employee_type) + + def clean(self, *args, **kwargs): + super().clean(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + if request and request.POST: + company = request.POST.getlist("company_id", None) + employee_type = request.POST.get("employee_type", None) + if ( + EmployeeType.objects.filter( + company_id__id__in=company, employee_type=employee_type + ) + .exclude(id=self.id) + .exists() + ): + raise ValidationError( + "This employee type already exists in this company" + ) + return + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.clean(*args, **kwargs) + return self + + +class EmployeeShiftDay(models.Model): + """ + EmployeeShiftDay model + """ + + day = models.CharField(max_length=20, choices=DAY) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager() + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Employee Shift Day") + verbose_name_plural = _("Employee Shift Days") + + def __str__(self) -> str: + return str(_(self.day).capitalize()) + + +class EmployeeShift(HorillaModel): + """ + EmployeeShift model + """ + + employee_shift = models.CharField( + max_length=50, + null=False, + blank=False, + ) + days = models.ManyToManyField(EmployeeShiftDay, through="EmployeeShiftSchedule") + weekly_full_time = models.CharField( + max_length=6, + default="40:00", + null=True, + blank=True, + validators=[validate_time_format], + ) + full_time = models.CharField( + max_length=6, default="200:00", validators=[validate_time_format] + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + if apps.is_installed("attendance"): + grace_time_id = models.ForeignKey( + "attendance.GraceTime", + null=True, + blank=True, + related_name="employee_shift", + on_delete=models.PROTECT, + verbose_name=_("Grace Time"), + ) + + objects = HorillaCompanyManager("employee_shift__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Employee Shift") + verbose_name_plural = _("Employee Shifts") + + def __str__(self) -> str: + return str(self.employee_shift) + + def clean(self, *args, **kwargs): + super().clean(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + if request and request.POST: + company = request.POST.getlist("company_id", None) + employee_shift = request.POST.get("employee_shift", None) + if ( + EmployeeShift.objects.filter( + company_id__id__in=company, employee_shift=employee_shift + ) + .exclude(id=self.id) + .exists() + ): + raise ValidationError( + "This employee shift already exists in this company" + ) + return + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.clean(*args, **kwargs) + return self + + +from django.db.models import Case, When + + +class EmployeeShiftSchedule(HorillaModel): + """ + EmployeeShiftSchedule model + """ + + day = models.ForeignKey( + EmployeeShiftDay, + on_delete=models.PROTECT, + related_name="day_schedule", + verbose_name=_("Shift Day"), + ) + shift_id = models.ForeignKey( + EmployeeShift, on_delete=models.PROTECT, verbose_name=_("Shift") + ) + minimum_working_hour = models.CharField( + default="08:15", + max_length=5, + validators=[validate_time_format], + verbose_name=_("Minimum Working Hours"), + ) + start_time = models.TimeField(null=True, verbose_name=_("Start Time")) + end_time = models.TimeField(null=True, verbose_name=_("End Time")) + is_night_shift = models.BooleanField(default=False, verbose_name=_("Night Shift")) + is_auto_punch_out_enabled = models.BooleanField( + default=False, + verbose_name=_("Enable Automatic Check Out"), + help_text=_("Enable this to trigger automatic check out."), + ) + auto_punch_out_time = models.TimeField( + null=True, + blank=True, + verbose_name=_("Automatic Check Out Time"), + help_text=_( + "Time at which the horilla will automatically check out the employee attendance if they forget." + ), + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + + objects = HorillaCompanyManager("shift_id__employee_shift__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Employee Shift Schedule") + verbose_name_plural = _("Employee Shift Schedules") + unique_together = [["shift_id", "day"]] + ordering = [ + Case( + When(day__day="monday", then=0), + When(day__day="tuesday", then=1), + When(day__day="wednesday", then=2), + When(day__day="thursday", then=3), + When(day__day="friday", then=4), + When(day__day="saturday", then=5), + When(day__day="sunday", then=6), + default=7, + ) + ] + + def __str__(self) -> str: + return f"{self.shift_id.employee_shift} {self.day}" + + def save(self, *args, **kwargs): + if self.start_time and self.end_time: + self.is_night_shift = self.start_time > self.end_time + super().save(*args, **kwargs) + + +class RotatingShift(HorillaModel): + """ + RotatingShift model + """ + + name = models.CharField(max_length=50) + employee_id = models.ManyToManyField( + "employee.Employee", through="RotatingShiftAssign", verbose_name=_("Employee") + ) + shift1 = models.ForeignKey( + EmployeeShift, + related_name="shift1", + on_delete=models.PROTECT, + verbose_name=_("Shift 1"), + blank=True, + null=True, + ) + shift2 = models.ForeignKey( + EmployeeShift, + related_name="shift2", + on_delete=models.PROTECT, + verbose_name=_("Shift 2"), + blank=True, + null=True, + ) + additional_data = models.JSONField( + default=dict, + blank=True, + null=True, + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Rotating Shift") + verbose_name_plural = _("Rotating Shifts") + + def __str__(self) -> str: + return str(self.name) + + def clean(self): + + additional_shifts = ( + self.additional_data.get("additional_shifts", []) + if self.additional_data + else [] + ) + + if additional_shifts and self.shift1 == self.shift2: + raise ValidationError(_("Select different shift continuously")) + + # ---------------- Removed the validation for same shifts to be continously added ---------------- + + # if additional_shifts and str(self.shift2.id) == additional_shifts[0]: + # raise ValidationError(_("Select different shift continuously")) + + # if additional_shifts and str(self.shift1.id) == additional_shifts[-1]: + # raise ValidationError(_("Select different shift continuously")) + + # for i in range(len(additional_shifts) - 1): + # if additional_shifts[i] and additional_shifts[i + 1]: + # if additional_shifts[i] == additional_shifts[i + 1]: + # raise ValidationError(_("Select different shift continuously")) + + def additional_shifts(self): + additional_data = self.additional_data + if additional_data: + additional_shift_ids = additional_data.get("additional_shifts") + if additional_shift_ids: + unique_ids = set(additional_shift_ids) + shifts_dict = { + shift.id: shift + for shift in EmployeeShift.objects.filter(id__in=unique_ids) + } + additional_shifts = [] + for shift_id in additional_shift_ids: + if shift_id: + additional_shifts.append(shifts_dict[int(shift_id)]) + else: + additional_shifts.append(None) + else: + additional_shifts = None + else: + additional_shifts = None + return additional_shifts + + def total_shifts(self): + total_shifts = [] + total_shifts += [self.shift1, self.shift2] + if self.additional_shifts(): + total_shifts += list(self.additional_shifts()) + + return total_shifts + + +class RotatingShiftAssign(HorillaModel): + """ + RotatingShiftAssign model + """ + + employee_id = models.ForeignKey( + "employee.Employee", on_delete=models.PROTECT, verbose_name=_("Employee") + ) + rotating_shift_id = models.ForeignKey( + RotatingShift, on_delete=models.PROTECT, verbose_name=_("Rotating Shift") + ) + start_date = models.DateField( + default=django.utils.timezone.now, verbose_name=_("Start Date") + ) + next_change_date = models.DateField(null=True, verbose_name=_("Next Switch")) + current_shift = models.ForeignKey( + EmployeeShift, + on_delete=models.PROTECT, + null=True, + related_name="current_shift", + verbose_name=_("Current Shift"), + ) + next_shift = models.ForeignKey( + EmployeeShift, + on_delete=models.PROTECT, + null=True, + related_name="next_shift", + verbose_name=_("Next Shift"), + ) + based_on = models.CharField( + max_length=10, + choices=BASED_ON, + null=False, + blank=False, + verbose_name=_("Based On"), + ) + rotate_after_day = models.IntegerField( + null=True, blank=True, default=7, verbose_name=_("Rotate After Day") + ) + rotate_every_weekend = models.CharField( + max_length=10, + default="monday", + choices=DAY, + blank=True, + null=True, + verbose_name=_("Rotate Every Weekend"), + ) + rotate_every = models.CharField( + max_length=10, + blank=True, + null=True, + default="1", + choices=DAY_DATE, + verbose_name=_("Rotate Every Month"), + ) + additional_data = models.JSONField( + default=dict, + blank=True, + null=True, + ) + history = HorillaAuditLog( + related_name="history_set", + bases=[ + HorillaAuditInfo, + ], + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Rotating Shift Assign") + verbose_name_plural = _("Rotating Shift Assigns") + ordering = ["-next_change_date", "-employee_id__employee_first_name"] + + def clean(self): + if self.is_active and self.employee_id_id is not None: + # Check if any other active record with the same parent already exists + siblings = RotatingShiftAssign.objects.filter( + is_active=True, employee_id__id=self.employee_id_id + ) + if siblings.exists() and siblings.first().id != self.id: + raise ValidationError(_("Only one active record allowed per employee")) + if self.start_date < django.utils.timezone.now().date(): + raise ValidationError(_("Date must be greater than or equal to today")) + + +class BaserequestFile(models.Model): + file = models.FileField(upload_to=upload_path) + objects = models.Manager() + + +class WorkTypeRequest(HorillaModel): + """ + WorkTypeRequest model + """ + + employee_id = models.ForeignKey( + "employee.Employee", + on_delete=models.PROTECT, + null=True, + related_name="work_type_request", + verbose_name=_("Employee"), + ) + work_type_id = models.ForeignKey( + WorkType, + on_delete=models.PROTECT, + related_name="requested_work_type", + verbose_name=_("Requesting Work Type"), + ) + previous_work_type_id = models.ForeignKey( + WorkType, + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="previous_work_type", + verbose_name=_("Previous Work Type"), + ) + requested_date = models.DateField( + null=True, default=django.utils.timezone.now, verbose_name=_("Requested Date") + ) + requested_till = models.DateField( + null=True, blank=True, verbose_name=_("Requested Till") + ) + description = models.TextField(null=True, verbose_name=_("Description")) + is_permanent_work_type = models.BooleanField( + default=False, verbose_name=_("Permanent Request") + ) + approved = models.BooleanField(default=False, verbose_name=_("Approved")) + canceled = models.BooleanField(default=False, verbose_name=_("Canceled")) + work_type_changed = models.BooleanField(default=False) + history = HorillaAuditLog( + related_name="history_set", + bases=[ + HorillaAuditInfo, + ], + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Work Type Request") + verbose_name_plural = _("Work Type Requests") + permissions = ( + ("approve_worktyperequest", "Approve Work Type Request"), + ("cancel_worktyperequest", "Cancel Work Type Request"), + ) + ordering = [ + "-id", + ] + + def delete(self, *args, **kwargs): + request = getattr(_thread_locals, "request", None) + if not self.approved: + super().delete(*args, **kwargs) + else: + if request: + clear_messages(request) + messages.warning(request, "The request entry cannot be deleted.") + + def is_any_work_type_request_exists(self): + approved_work_type_requests_range = WorkTypeRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__range=[self.requested_date, self.requested_till], + requested_till__range=[self.requested_date, self.requested_till], + ).exclude(id=self.id) + if approved_work_type_requests_range: + return True + approved_work_type_requests = WorkTypeRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__lte=self.requested_date, + requested_till__gte=self.requested_date, + ).exclude(id=self.id) + if approved_work_type_requests: + return True + if self.requested_till: + approved_work_type_requests_2 = WorkTypeRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__lte=self.requested_till, + requested_till__gte=self.requested_till, + ).exclude(id=self.id) + if approved_work_type_requests_2: + return True + approved_permanent_req = WorkTypeRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__exact=self.requested_date, + ) + if approved_permanent_req: + return True + return False + + def clean(self): + request = getattr(horilla_middlewares._thread_locals, "request", None) + if not request.user.is_superuser: + if self.requested_date < django.utils.timezone.now().date(): + raise ValidationError(_("Date must be greater than or equal to today")) + if self.requested_till and self.requested_till < self.requested_date: + raise ValidationError( + _("End date must be greater than or equal to start date") + ) + if self.is_any_work_type_request_exists(): + raise ValidationError( + _("A work type request already exists during this time period.") + ) + if not self.is_permanent_work_type: + if not self.requested_till: + raise ValidationError(_("Requested till field is required.")) + + def request_status(self): + return ( + _("Rejected") + if self.canceled + else (_("Approved") if self.approved else _("Requested")) + ) + + def __str__(self) -> str: + return f"{self.employee_id.employee_first_name} \ + {self.employee_id.employee_last_name} - {self.requested_date}" + + +class WorkTypeRequestComment(HorillaModel): + """ + WorkTypeRequestComment Model + """ + + from employee.models import Employee + + request_id = models.ForeignKey(WorkTypeRequest, on_delete=models.CASCADE) + employee_id = models.ForeignKey(Employee, on_delete=models.CASCADE) + comment = models.TextField(null=True, verbose_name=_("Comment")) + files = models.ManyToManyField(BaserequestFile, blank=True) + objects = models.Manager() + + def __str__(self) -> str: + return f"{self.comment}" + + +class ShiftRequest(HorillaModel): + """ + ShiftRequest model + """ + + employee_id = models.ForeignKey( + "employee.Employee", + on_delete=models.PROTECT, + null=True, + related_name="shift_request", + verbose_name=_("Employee"), + ) + shift_id = models.ForeignKey( + EmployeeShift, + on_delete=models.PROTECT, + related_name="requested_shift", + verbose_name=_("Requesting Shift"), + ) + previous_shift_id = models.ForeignKey( + EmployeeShift, + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="previous_shift", + verbose_name=_("Previous Shift"), + ) + requested_date = models.DateField( + null=True, default=django.utils.timezone.now, verbose_name=_("Requested Date") + ) + reallocate_to = models.ForeignKey( + "employee.Employee", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="reallocate_shift_request", + verbose_name=_("Reallocate Employee"), + ) + reallocate_approved = models.BooleanField(default=False, verbose_name=_("Approved")) + reallocate_canceled = models.BooleanField(default=False, verbose_name=_("Canceled")) + requested_till = models.DateField( + null=True, blank=True, verbose_name=_("Requested Till") + ) + description = models.TextField(null=True, verbose_name=_("Description")) + is_permanent_shift = models.BooleanField( + default=False, verbose_name=_("Permanent Request") + ) + approved = models.BooleanField(default=False, verbose_name=_("Approved")) + canceled = models.BooleanField(default=False, verbose_name=_("Canceled")) + shift_changed = models.BooleanField(default=False) + history = HorillaAuditLog( + related_name="history_set", + bases=[ + HorillaAuditInfo, + ], + ) + objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Shift Request") + verbose_name_plural = _("Shift Requests") + permissions = ( + ("approve_shiftrequest", "Approve Shift Request"), + ("cancel_shiftrequest", "Cancel Shift Request"), + ) + ordering = [ + "-id", + ] + + def clean(self): + + request = getattr(horilla_middlewares._thread_locals, "request", None) + if not request.user.is_superuser: + if not self.pk and self.requested_date < django.utils.timezone.now().date(): + raise ValidationError(_("Date must be greater than or equal to today")) + if self.requested_till and self.requested_till < self.requested_date: + raise ValidationError( + _("End date must be greater than or equal to start date") + ) + if self.is_any_request_exists(): + raise ValidationError( + _("An approved shift request already exists during this time period.") + ) + if not self.is_permanent_shift: + if not self.requested_till: + raise ValidationError(_("Requested till field is required.")) + + def is_any_request_exists(self): + approved_shift_requests_range = ShiftRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__range=[self.requested_date, self.requested_till], + requested_till__range=[self.requested_date, self.requested_till], + ).exclude(id=self.id) + if approved_shift_requests_range: + return True + approved_shift_requests = ShiftRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__lte=self.requested_date, + requested_till__gte=self.requested_date, + ).exclude(id=self.id) + if approved_shift_requests: + return True + if self.requested_till: + approved_shift_requests_2 = ShiftRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__lte=self.requested_till, + requested_till__gte=self.requested_till, + ).exclude(id=self.id) + if approved_shift_requests_2: + return True + approved_permanent_req = ShiftRequest.objects.filter( + employee_id=self.employee_id, + approved=True, + canceled=False, + requested_date__exact=self.requested_date, + ) + if approved_permanent_req: + return True + return False + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + request = getattr(_thread_locals, "request", None) + if not self.approved: + super().delete(*args, **kwargs) + else: + if request: + clear_messages(request) + messages.warning(request, "The request entry cannot be deleted.") + + def __str__(self) -> str: + return f"{self.employee_id.employee_first_name} \ + {self.employee_id.employee_last_name} - {self.requested_date}" + + +class ShiftRequestComment(HorillaModel): + """ + ShiftRequestComment Model + """ + + from employee.models import Employee + + request_id = models.ForeignKey(ShiftRequest, on_delete=models.CASCADE) + employee_id = models.ForeignKey(Employee, on_delete=models.CASCADE) + files = models.ManyToManyField(BaserequestFile, blank=True) + comment = models.TextField(null=True, verbose_name=_("Comment")) + objects = models.Manager() + + def __str__(self) -> str: + return f"{self.comment}" + + +class Tags(HorillaModel): + title = models.CharField(max_length=30) + color = models.CharField(max_length=30) + company_id = models.ForeignKey( + Company, null=True, editable=False, on_delete=models.PROTECT + ) + objects = HorillaCompanyManager(related_company_field="company_id") + + class Meta: + verbose_name = _("Tag") + verbose_name_plural = _("Tags") + + def __str__(self): + return self.title + + +class HorillaMailTemplate(HorillaModel): + title = models.CharField(max_length=100, unique=True) + body = models.TextField() + company_id = models.ForeignKey( + Company, + null=True, + blank=True, + on_delete=models.CASCADE, + verbose_name=_("Company"), + ) + objects = HorillaCompanyManager(related_company_field="company_id") + + def __str__(self) -> str: + return f"{self.title}" + + +class DynamicEmailConfiguration(HorillaModel): + """ + SingletonModel to keep the mail server configurations + """ + + host = models.CharField(null=True, max_length=256, verbose_name=_("Email Host")) + + port = models.SmallIntegerField(null=True, verbose_name=_("Email Port")) + + from_email = models.EmailField( + null=True, max_length=256, verbose_name=_("Default From Email") + ) + + username = models.CharField( + null=True, + max_length=256, + verbose_name=_("Email Host Username"), + ) + + display_name = models.CharField( + null=True, + max_length=256, + verbose_name=_("Display Name"), + ) + + password = models.CharField( + null=True, + max_length=256, + verbose_name=_("Email Authentication Password"), + ) + + use_tls = models.BooleanField(default=True, verbose_name=_("Use TLS")) + + use_ssl = models.BooleanField(default=False, verbose_name=_("Use SSL")) + + fail_silently = models.BooleanField(default=False, verbose_name=_("Fail Silently")) + + is_primary = models.BooleanField( + default=False, verbose_name=_("Primary Mail Server") + ) + use_dynamic_display_name = models.BooleanField( + default=True, + help_text=_( + "By enabling this the display name will take from who triggered the mail" + ), + ) + + timeout = models.SmallIntegerField( + null=True, verbose_name=_("Email Send Timeout (seconds)") + ) + company_id = models.OneToOneField( + Company, on_delete=models.CASCADE, null=True, blank=True + ) + + def clean(self): + if self.use_ssl and self.use_tls: + raise ValidationError( + _( + '"Use TLS" and "Use SSL" are mutually exclusive, ' + "so only set one of those settings to True." + ) + ) + if not self.company_id and not self.is_primary: + raise ValidationError({"company_id": _("This field is required")}) + + def __str__(self): + return self.username + + def save(self, *args, **kwargs) -> None: + if self.is_primary: + DynamicEmailConfiguration.objects.filter(is_primary=True).update( + is_primary=False + ) + if not DynamicEmailConfiguration.objects.exists(): + self.is_primary = True + + super().save(*args, **kwargs) + servers_same_company = DynamicEmailConfiguration.objects.filter( + company_id=self.company_id + ).exclude(id=self.id) + if servers_same_company.exists(): + self.delete() + return + + class Meta: + verbose_name = _("Email Configuration") + + +FIELD_CHOICE = [ + ("", "---------"), + ("requested_days", _("Leave Requested Days")), +] +CONDITION_CHOICE = [ + ("equal", _("Equal (==)")), + ("notequal", _("Not Equal (!=)")), + ("range", _("Range")), + ("lt", _("Less Than (<)")), + ("gt", _("Greater Than (>)")), + ("le", _("Less Than or Equal To (<=)")), + ("ge", _("Greater Than or Equal To (>=)")), + ("icontains", _("Contains")), +] + + +class MultipleApprovalCondition(HorillaModel): + department = models.ForeignKey(Department, on_delete=models.CASCADE) + condition_field = models.CharField( + max_length=255, + choices=FIELD_CHOICE, + ) + condition_operator = models.CharField( + max_length=255, choices=CONDITION_CHOICE, null=True, blank=True + ) + condition_value = models.CharField( + max_length=100, + null=True, + blank=True, + verbose_name=_("Condition Value"), + ) + condition_start_value = models.CharField( + max_length=100, + null=True, + blank=True, + verbose_name=_("Starting Value"), + ) + condition_end_value = models.CharField( + max_length=100, + null=True, + blank=True, + verbose_name=_("Ending Value"), + ) + objects = models.Manager() + company_id = models.ForeignKey( + Company, + null=True, + blank=True, + on_delete=models.CASCADE, + verbose_name=_("Company"), + ) + + def __str__(self) -> str: + return f"{self.condition_field} {self.condition_operator}" + + def clean(self, *args, **kwargs): + if self.condition_value: + instance = MultipleApprovalCondition.objects.filter( + department=self.department, + condition_field=self.condition_field, + condition_operator=self.condition_operator, + condition_value=self.condition_value, + company_id=self.company_id, + ).exclude(id=self.pk) + if instance: + raise ValidationError( + _("A condition with the provided fields already exists") + ) + if self.condition_field == "requested_days": + if self.condition_operator != "range": + if not self.condition_value: + raise ValidationError( + { + "condition_operator": _( + "Please enter a numeric value for condition value" + ) + } + ) + try: + float_value = float(self.condition_value) + except ValueError as e: + raise ValidationError( + { + "condition_operator": _( + "Please enter a valid numeric value for the condition value when the condition field is Leave Requested Days." + ) + } + ) + else: + if not self.condition_start_value or not self.condition_end_value: + raise ValidationError( + { + "condition_operator": _( + "Please specify condition value range" + ) + } + ) + try: + start_value = float(self.condition_start_value) + except ValueError as e: + raise ValidationError( + { + "condition_operator": _( + "Please enter a valid numeric value for the starting value when the condition field is Leave Requested Days." + ) + } + ) + try: + end_value = float(self.condition_end_value) + except ValueError as e: + raise ValidationError( + { + "condition_operator": _( + "Please enter a valid numeric value for the ending value when the condition field is Leave Requested Days." + ) + } + ) + + if start_value == end_value: + raise ValidationError( + { + "condition_operator": _( + "End value must be different from the start value in a range." + ) + } + ) + if end_value <= start_value: + raise ValidationError( + { + "condition_operator": _( + "End value must be greater than the start value in a range." + ) + } + ) + super().clean(*args, **kwargs) + + def save(self, *args, **kwargs): + if self.condition_operator != "range": + self.condition_start_value = None + self.condition_end_value = None + else: + self.condition_value = None + super().save(*args, **kwargs) + + def approval_managers(self, *args, **kwargs): + managers = [] + from employee.models import Employee + + queryset = MultipleApprovalManagers.objects.filter( + condition_id=self.pk + ).order_by("sequence") + for query in queryset: + emp_id = query.employee_id + employee = ( + query.reporting_manager + if not emp_id + else Employee.objects.get(id=emp_id) + ) + managers.append(employee) + + return managers + + +class MultipleApprovalManagers(models.Model): + condition_id = models.ForeignKey( + MultipleApprovalCondition, on_delete=models.CASCADE + ) + sequence = models.IntegerField(null=False, blank=False) + employee_id = models.IntegerField(null=True, blank=True) + reporting_manager = models.CharField(max_length=100, null=True, blank=True) + objects = models.Manager() + + class Meta: + verbose_name = _("Multiple Approval Managers") + verbose_name_plural = _("Multiple Approval Managers") + + def get_manager(self): + manager = self.employee_id + if manager: + manager = self.reporting_manager.replace("_", " ").title() + return manager + + +class DynamicPagination(models.Model): + """ + model for storing pagination for employees + """ + + from django.contrib.auth.models import User + from django.core.validators import MinValueValidator + + user_id = models.OneToOneField( + User, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="dynamic_pagination", + verbose_name=_("User"), + ) + pagination = models.IntegerField(default=50, validators=[MinValueValidator(1)]) + objects = models.Manager() + + def save(self, *args, **kwargs): + request = getattr(_thread_locals, "request", None) + user = request.user + self.user_id = user + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.user_id}|{self.pagination}" + + +class Attachment(models.Model): + """ + Attachment model for multiple attachments in announcements. + """ + + file = models.FileField(upload_to=upload_path) + + def __str__(self): + return self.file.name + + +class AnnouncementExpire(models.Model): + """ + This model for setting a expire days for announcement if no expire date for announcement + """ + + days = models.IntegerField(null=True, blank=True, default=30) + objects = models.Manager() + + +class Announcement(HorillaModel): + """ + Announcement Model for storing all announcements. + """ + + from employee.models import Employee + + model_employee = Employee + + title = models.CharField(max_length=100) + description = models.TextField(null=True) + attachments = models.ManyToManyField( + Attachment, related_name="announcement_attachments", blank=True + ) + expire_date = models.DateField(null=True, blank=True) + employees = models.ManyToManyField( + Employee, related_name="announcement_employees", blank=True + ) + department = models.ManyToManyField(Department, blank=True) + job_position = models.ManyToManyField( + JobPosition, blank=True, verbose_name=_("Job Position") + ) + company_id = models.ManyToManyField( + Company, blank=True, related_name="announcement", verbose_name=_("Company") + ) + disable_comments = models.BooleanField( + default=False, verbose_name=_("Disable Comments") + ) + public_comments = models.BooleanField( + default=True, + verbose_name=_("Show Comments to All"), + help_text=_("If enabled, all employees can view each other's comments."), + ) + + filtered_employees = models.ManyToManyField( + Employee, related_name="announcement_filtered_employees", editable=False + ) + objects = HorillaCompanyManager(related_company_field="company_id") + + class Meta: + verbose_name = _("Announcement") + verbose_name_plural = _("Announcements") + + def get_views(self): + """ + This method is used to get the view count of the announcement + """ + return self.announcementview_set.filter(viewed=True) + + def viewed_by(self): + + viewed_by = AnnouncementView.objects.filter( + announcement_id__id=self.id, viewed=True + ) + viewed_emp = [] + for i in viewed_by: + viewed_emp.append(i.user) + return viewed_emp + + def save(self, *args, **kwargs): + """ + if comments are disabled, force public comments to be false + """ + if self.disable_comments: + self.public_comments = False + super().save(*args, **kwargs) + + def __str__(self): + return self.title + + +class AnnouncementComment(HorillaModel): + """ + AnnouncementComment Model + """ + + from employee.models import Employee + + announcement_id = models.ForeignKey(Announcement, on_delete=models.CASCADE) + employee_id = models.ForeignKey(Employee, on_delete=models.CASCADE) + comment = models.TextField(null=True, verbose_name=_("Comment"), max_length=255) + objects = models.Manager() + + +class AnnouncementView(models.Model): + """ + Announcement View Model + """ + + user = models.ForeignKey(User, on_delete=models.CASCADE) + announcement = models.ForeignKey(Announcement, on_delete=models.CASCADE) + viewed = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True, null=True) + objects = models.Manager() + + +class EmailLog(models.Model): + """ + EmailLog Keeping model + """ + + statuses = [("sent", "Sent"), ("failed", "Failed")] + subject = models.CharField(max_length=255) + body = models.TextField(max_length=255) + from_email = models.EmailField() + to = models.EmailField() + status = models.CharField(max_length=6, choices=statuses) + created_at = models.DateTimeField(auto_now_add=True) + objects = models.Manager() + company_id = models.ForeignKey( + Company, on_delete=models.CASCADE, null=True, editable=False + ) + + +class DriverViewed(models.Model): + """ + Model to store driver viewed status + """ + + choices = [ + ("dashboard", "dashboard"), + ("pipeline", "pipeline"), + ("settings", "settings"), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE) + viewed = models.CharField(max_length=10, choices=choices) + + def user_viewed(self): + """ + This method is used to access all the viewd driver + """ + return self.user.driverviewed_set.values_list("viewed", flat=True) + + +class DashboardEmployeeCharts(HorillaModel): + from employee.models import Employee + + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + charts = models.JSONField( + verbose_name=_("Excluded Charts"), default=list, blank=True, null=True + ) + + class Meta: + verbose_name = _("Dashboard Employee Charts") + verbose_name_plural = _("Dashboard Employee Charts") + + def __str__(self): + return f"{self.employee} - charts" + + +class BiometricAttendance(models.Model): + is_installed = models.BooleanField(default=False) + company_id = models.ForeignKey( + Company, + null=True, + editable=False, + on_delete=models.PROTECT, + related_name="biometric_enabled_company", + ) + objects = models.Manager() + + def __str__(self): + return f"{self.is_installed}" + + +def default_additional_data(): + return {"allowed_ips": []} + + +class AttendanceAllowedIP(models.Model): + """ + Represents client IP addresses that are allowed to mark attendance. + Usage: + - This model is used to store IP addresses that are permitted to access the attendance system. + - It ensures that only authorized IP addresses can mark attendance. + """ + + is_enabled = models.BooleanField(default=False) + additional_data = models.JSONField( + null=True, blank=True, default=default_additional_data + ) + + def clean(self): + """ + Validate that all entries in `allowed_ips` are either valid IP addresses or network prefixes. + """ + allowed_ips = self.additional_data.get("allowed_ips", []) + for ip in allowed_ips: + try: + ipaddress.ip_network(ip) + except ValueError: + raise ValidationError(f"Invalid IP address or network prefix: {ip}") + + def __str__(self): + return f"AttendanceAllowedIP - {self.is_enabled}" + + +class TrackLateComeEarlyOut(HorillaModel): + is_enable = models.BooleanField( + default=True, + verbose_name=_("Enable"), + help_text=_( + "By enabling this, you track the late comes and early outs of employees in their attendance." + ), + ) + + class Meta: + verbose_name = _("Track Late Come Early Out") + verbose_name_plural = _("Track Late Come Early Outs") + + def __str__(self): + tracking = _("enabled") if self.is_enable else _("disabled") + return f"Tracking late come early out {tracking}" + + def save(self, *args, **kwargs): + if not self.pk and TrackLateComeEarlyOut.objects.exists(): + raise ValidationError( + _("Only one TrackLateComeEarlyOut instance is allowed.") + ) + return super().save(*args, **kwargs) + + +class Holidays(HorillaModel): + name = models.CharField(max_length=30, null=False, verbose_name=_("Name")) + start_date = models.DateField(verbose_name=_("Start Date")) + end_date = models.DateField(null=True, blank=True, verbose_name=_("End Date")) + recurring = models.BooleanField(default=False, verbose_name=_("Recurring")) + company_id = models.ForeignKey( + Company, + null=True, + on_delete=models.PROTECT, + verbose_name=_("Company"), + ) + objects = HorillaCompanyManager(related_company_field="company_id") + + class Meta: + verbose_name = _("Holiday") + verbose_name_plural = _("Holidays") + + def __str__(self): + return self.name + + def today_holidays(today=None) -> models.QuerySet: + """ + Retrieve holidays that overlap with the given date (default is today). + + Args: + today (date, optional): The date to check for holidays. Defaults to the current date. + + Returns: + QuerySet: A queryset of `Holidays` instances where the given date falls between + `start_date` and `end_date` (inclusive). + """ + today = today or date.today() + return Holidays.objects.filter(start_date__lte=today, end_date__gte=today) + + +class CompanyLeaves(HorillaModel): + based_on_week = models.CharField( + max_length=100, + choices=WEEKS, + blank=True, + null=True, + verbose_name=_("Based On Week"), + ) + based_on_week_day = models.CharField( + max_length=100, choices=WEEK_DAYS, verbose_name=_("Based On Week Day") + ) + company_id = models.ForeignKey( + Company, null=True, on_delete=models.PROTECT, verbose_name=_("Company") + ) + objects = HorillaCompanyManager() + + class Meta: + unique_together = ("based_on_week", "based_on_week_day") + verbose_name = _("Company Leave") + verbose_name_plural = _("Company Leaves") + + def __str__(self): + return f"{dict(WEEK_DAYS).get(self.based_on_week_day)} | {dict(WEEKS).get(self.based_on_week)}" + + +class PenaltyAccounts(HorillaModel): + """ + LateComeEarlyOutPenaltyAccount + """ + + employee_id = models.ForeignKey( + "employee.Employee", + on_delete=models.PROTECT, + related_name="penalty_accounts", + editable=False, + verbose_name="Employee", + null=True, + ) + if apps.is_installed("attendance"): + late_early_id = models.ForeignKey( + "attendance.AttendanceLateComeEarlyOut", + on_delete=models.CASCADE, + null=True, + editable=False, + ) + if apps.is_installed("leave"): + leave_request_id = models.ForeignKey( + "leave.LeaveRequest", null=True, on_delete=models.CASCADE, editable=False + ) + leave_type_id = models.ForeignKey( + "leave.LeaveType", + on_delete=models.DO_NOTHING, + blank=True, + null=True, + verbose_name="Leave type", + ) + minus_leaves = models.FloatField(default=0.0, null=True) + deduct_from_carry_forward = models.BooleanField(default=False) + penalty_amount = models.FloatField(default=0.0, null=True) + + def clean(self) -> None: + super().clean() + if apps.is_installed("leave") and not self.leave_type_id and self.minus_leaves: + raise ValidationError( + {"leave_type_id": _("Specify the leave type to deduct the leave.")} + ) + if apps.is_installed("leave") and self.leave_type_id and not self.minus_leaves: + raise ValidationError( + { + "minus_leaves": _( + "If a leave type is chosen for a penalty, minus leaves are required." + ) + } + ) + if not self.minus_leaves and not self.penalty_amount: + raise ValidationError( + { + "leave_type_id": _( + "Either minus leaves or a penalty amount is required" + ) + } + ) + + if ( + self.minus_leaves or self.deduct_from_carry_forward + ) and not self.leave_type_id: + raise ValidationError({"leave_type_id": _("Leave type is required")}) + return + + class Meta: + ordering = ["-created_at"] + verbose_name = _("Penalty Account") + verbose_name_plural = _("Penalty Accounts") + + +class NotificationSound(models.Model): + from employee.models import Employee + + employee = models.OneToOneField( + Employee, on_delete=models.CASCADE, related_name="notification_sound" + ) + sound_enabled = models.BooleanField(default=False) + + +User.add_to_class("is_new_employee", models.BooleanField(default=False)) diff --git a/base/request_and_approve.py b/base/request_and_approve.py new file mode 100644 index 0000000..f1d3843 --- /dev/null +++ b/base/request_and_approve.py @@ -0,0 +1,56 @@ +""" +views.py + +This module is used to map url patterns with request and approve methods in Dashboard. +""" + +import json + +from django.apps import apps +from django.shortcuts import render + +from base.methods import filtersubordinates, paginator_qry +from base.models import ShiftRequest, WorkTypeRequest +from horilla.decorators import login_required + + +@login_required +def dashboard_shift_request(request): + page_number = request.GET.get("page") + previous_data = request.GET.urlencode() + requests = ShiftRequest.objects.filter( + approved=False, canceled=False, employee_id__is_active=True + ) + requests = filtersubordinates(request, requests, "base.add_shiftrequest") + requests_ids = json.dumps([instance.id for instance in requests]) + requests = paginator_qry(requests, page_number) + return render( + request, + "request_and_approve/shift_request.html", + { + "requests": requests, + "requests_ids": requests_ids, + "pd": previous_data, + }, + ) + + +@login_required +def dashboard_work_type_request(request): + page_number = request.GET.get("page") + previous_data = request.GET.urlencode() + requests = WorkTypeRequest.objects.filter( + approved=False, canceled=False, employee_id__is_active=True + ) + requests = filtersubordinates(request, requests, "base.add_worktyperequest") + requests_ids = json.dumps([instance.id for instance in requests]) + requests = paginator_qry(requests, page_number) + return render( + request, + "request_and_approve/work_type_request.html", + { + "requests": requests, + "requests_ids": requests_ids, + "pd": previous_data, + }, + ) diff --git a/base/scheduler.py b/base/scheduler.py new file mode 100644 index 0000000..f7a23a6 --- /dev/null +++ b/base/scheduler.py @@ -0,0 +1,501 @@ +import calendar +import sys +from datetime import date, datetime, timedelta + +from apscheduler.schedulers.background import BackgroundScheduler +from django.urls import reverse + +from notifications.signals import notify + + +def update_rotating_work_type_assign(rotating_work_type, new_date): + """ + Here will update the employee work information details and send notification + """ + from django.contrib.auth.models import User + + employee = rotating_work_type.employee_id + employee_work_info = employee.employee_work_info + work_type1 = rotating_work_type.rotating_work_type_id.work_type1 + work_type2 = rotating_work_type.rotating_work_type_id.work_type2 + additional_work_types = ( + rotating_work_type.rotating_work_type_id.additional_work_types() + ) + if additional_work_types is None: + total_rotate_work_types = [work_type1, work_type2] + else: + total_rotate_work_types = [work_type1, work_type2] + list(additional_work_types) + next_work_type_index = rotating_work_type.additional_data.get( + "next_work_type_index", 0 + ) + next_work_type = total_rotate_work_types[next_work_type_index] + if next_work_type_index < len(total_rotate_work_types) - 1: + next_work_type_index += 1 + else: + next_work_type_index = 0 + + rotating_work_type.additional_data["next_work_type_index"] = next_work_type_index + employee_work_info.work_type_id = rotating_work_type.next_work_type + employee_work_info.save() + rotating_work_type.next_change_date = new_date + rotating_work_type.current_work_type = rotating_work_type.next_work_type + rotating_work_type.next_work_type = next_work_type + rotating_work_type.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = rotating_work_type.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Your Work Type has been changed.", + verb_ar="لقد تغير نوع عملك.", + verb_de="Ihre Art der Arbeit hat sich geändert.", + verb_es="Su tipo de trabajo ha sido cambiado.", + verb_fr="Votre type de travail a été modifié.", + icon="infinite", + redirect=reverse("employee-profile"), + ) + return + + +def work_type_rotate_after(rotating_work_work_type): + """ + This method for rotate work type based on after day + """ + date_today = datetime.now() + switch_date = rotating_work_work_type.next_change_date + if switch_date.strftime("%Y-%m-%d") == date_today.strftime("%Y-%m-%d"): + new_date = date_today + timedelta(days=rotating_work_work_type.rotate_after_day) + update_rotating_work_type_assign(rotating_work_work_type, new_date) + return + + +def work_type_rotate_weekend(rotating_work_type): + """ + This method for rotate work type based on weekend + """ + date_today = datetime.now() + switch_date = rotating_work_type.next_change_date + if switch_date.strftime("%Y-%m-%d") == date_today.strftime("%Y-%m-%d"): + day = datetime.now().strftime("%A").lower() + switch_day = rotating_work_type.rotate_every_weekend + if day == switch_day: + new_date = date_today + timedelta(days=7) + update_rotating_work_type_assign(rotating_work_type, new_date) + return + + +def work_type_rotate_every(rotating_work_type): + """ + This method for rotate work type based on every month + """ + date_today = datetime.now() + switch_date = rotating_work_type.next_change_date + day_date = rotating_work_type.rotate_every + if switch_date.strftime("%Y-%m-%d") == date_today.strftime("%Y-%m-%d"): + if day_date == switch_date.strftime("%d").lstrip("0"): + new_date = date_today.replace(month=date_today.month + 1) + update_rotating_work_type_assign(rotating_work_type, new_date) + elif day_date == "last": + year = date_today.strftime("%Y") + month = date_today.strftime("%m") + last_day = calendar.monthrange(int(year), int(month) + 1)[1] + new_date = datetime(int(year), int(month) + 1, last_day) + update_rotating_work_type_assign(rotating_work_type, new_date) + return + + +def rotate_work_type(): + """ + This method will identify the based on condition to the rotating shift assign + and redirect to the chunk method to execute. + """ + from base.models import RotatingWorkTypeAssign + + rotating_work_types = RotatingWorkTypeAssign.objects.filter(is_active=True) + for rotating_work_type in rotating_work_types: + based_on = rotating_work_type.based_on + if based_on == "after": + work_type_rotate_after(rotating_work_type) + elif based_on == "weekly": + work_type_rotate_weekend(rotating_work_type) + elif based_on == "monthly": + work_type_rotate_every(rotating_work_type) + return + + +def update_rotating_shift_assign(rotating_shift, new_date): + """ + Here will update the employee work information and send notification + """ + from django.contrib.auth.models import User + + next_shift_index = 0 + employee = rotating_shift.employee_id + employee_work_info = employee.employee_work_info + rotating_shift_id = rotating_shift.rotating_shift_id + shift1 = rotating_shift_id.shift1 + shift2 = rotating_shift_id.shift2 + additional_shifts = rotating_shift_id.additional_shifts() + if additional_shifts is None: + total_rotate_shifts = [shift1, shift2] + else: + total_rotate_shifts = [shift1, shift2] + list(additional_shifts) + next_shift_index = rotating_shift.additional_data.get("next_shift_index") + next_shift = total_rotate_shifts[next_shift_index] + if next_shift_index < len(total_rotate_shifts) - 1: + next_shift_index += 1 + else: + next_shift_index = 0 # Wrap around to the beginning of the list + rotating_shift.additional_data["next_shift_index"] = next_shift_index + employee_work_info.shift_id = rotating_shift.next_shift + employee_work_info.save() + rotating_shift.next_change_date = new_date + rotating_shift.current_shift = rotating_shift.next_shift + rotating_shift.next_shift = next_shift + rotating_shift.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = rotating_shift.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Your shift has been changed.", + verb_ar="تم تغيير التحول الخاص بك.", + verb_de="Ihre Schicht wurde geändert.", + verb_es="Tu turno ha sido cambiado.", + verb_fr="Votre quart de travail a été modifié.", + icon="infinite", + redirect=reverse("employee-profile"), + ) + return + + +def shift_rotate_after_day(rotating_shift, today): + """ + This method for rotate shift based on after day + """ + switch_date = rotating_shift.next_change_date + if switch_date == today: + new_date = today + timedelta(days=rotating_shift.rotate_after_day) + update_rotating_shift_assign(rotating_shift, new_date) + return + + +def shift_rotate_weekend(rotating_shift, today): + """ + This method for rotate shift based on weekend + """ + switch_date = rotating_shift.next_change_date + if switch_date == today: + day = today.strftime("%A").lower() + switch_day = rotating_shift.rotate_every_weekend + if day == switch_day: + new_date = today + timedelta(days=7) + update_rotating_shift_assign(rotating_shift, new_date) + return + + +def shift_rotate_every(rotating_shift, today): + """ + This method for rotate shift based on every month + """ + switch_date = rotating_shift.next_change_date + day_date = rotating_shift.rotate_every + if switch_date == today: + if day_date == switch_date.strftime("%d").lstrip("0"): + new_date = today.replace(month=today.month + 1) + update_rotating_shift_assign(rotating_shift, new_date) + elif day_date == "last": + year = today.year + month = today.month + last_day = calendar.monthrange(int(year), int(month) + 1)[1] + new_date = datetime(int(year), int(month) + 1, last_day) + update_rotating_shift_assign(rotating_shift, new_date) + return + + +def rotate_shift(): + """ + This method will identify the based on condition to the rotating shift assign + and redirect to the chunk method to execute. + """ + from base.models import RotatingShiftAssign + + rotating_shifts = RotatingShiftAssign.objects.filter(is_active=True) + today = datetime.now().date() + r_shifts = rotating_shifts.filter(start_date__lte=today) + rotating_shifts_modified = None + for r_shift in r_shifts: + emp_shift = rotating_shifts.filter( + employee_id=r_shift.employee_id, start_date__lte=today + ).exclude(id=r_shift.id) + rotating_shifts_modified = rotating_shifts.exclude( + id__in=emp_shift.values_list("id", flat=True) + ) + emp_shift.update(is_active=False) + + for rotating_shift in rotating_shifts_modified: + based_on = rotating_shift.based_on + # after day condition + if based_on == "after": + shift_rotate_after_day(rotating_shift, today) + # weekly condition + elif based_on == "weekly": + shift_rotate_weekend(rotating_shift, today) + # monthly condition + elif based_on == "monthly": + shift_rotate_every(rotating_shift, today) + + return + + +def switch_shift(): + """ + This method change employees shift information regards to the shift request + """ + from django.contrib.auth.models import User + + from base.models import ShiftRequest + + today = date.today() + + shift_requests = ShiftRequest.objects.filter( + canceled=False, approved=True, requested_date__exact=today, shift_changed=False + ) + if shift_requests: + for request in shift_requests: + work_info = request.employee_id.employee_work_info + # updating requested shift to the employee work information. + work_info.shift_id = request.shift_id + work_info.save() + request.approved = True + request.shift_changed = True + request.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = request.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Shift Changes notification", + verb_ar="التحول تغيير الإخطار", + verb_de="Benachrichtigung über Schichtänderungen", + verb_es="Notificación de cambios de turno", + verb_fr="Notification des changements de quart de travail", + icon="refresh", + redirect=reverse("employee-profile"), + ) + return + + +def undo_shift(): + """ + This method undo previous employees shift information regards to the shift request + """ + from django.contrib.auth.models import User + + from base.models import ShiftRequest + + today = date.today() + # here will get all the active shift requests + shift_requests = ShiftRequest.objects.filter( + canceled=False, + approved=True, + requested_till__lt=today, + is_active=True, + shift_changed=True, + ) + if shift_requests: + for request in shift_requests: + work_info = request.employee_id.employee_work_info + work_info.shift_id = request.previous_shift_id + work_info.save() + # making the instance in-active + request.is_active = False + request.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = request.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Shift changes notification, Requested date expired.", + verb_ar="التحول يغير الإخطار ، التاريخ المطلوب انتهت صلاحيته.", + verb_de="Benachrichtigung über Schichtänderungen, gewünschtes Datum abgelaufen.", + verb_es="Notificación de cambios de turno, Fecha solicitada vencida.", + verb_fr="Notification de changement d'équipe, la date demandée a expiré.", + icon="refresh", + redirect=reverse("employee-profile"), + ) + return + + +def switch_work_type(): + """ + This method change employees work type information regards to the work type request + """ + from django.contrib.auth.models import User + + from base.models import WorkTypeRequest + + today = date.today() + work_type_requests = WorkTypeRequest.objects.filter( + canceled=False, + approved=True, + requested_date__exact=today, + work_type_changed=False, + ) + for request in work_type_requests: + work_info = request.employee_id.employee_work_info + # updating requested work type to the employee work information. + work_info.work_type_id = request.work_type_id + work_info.save() + request.approved = True + request.work_type_changed = True + request.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = request.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Work Type Changes notification", + verb_ar="إخطار تغييرات نوع العمل", + verb_de="Benachrichtigung über Änderungen des Arbeitstyps", + verb_es="Notificación de cambios de tipo de trabajo", + verb_fr="Notification de changement de type de travail", + icon="swap-horizontal", + redirect=reverse("employee-profile"), + ) + return + + +def undo_work_type(): + """ + This method undo previous employees work type information regards to the work type request + """ + from django.contrib.auth.models import User + + from base.models import WorkTypeRequest + + today = date.today() + # here will get all the active work type requests + work_type_requests = WorkTypeRequest.objects.filter( + canceled=False, + approved=True, + requested_till__lt=today, + is_active=True, + work_type_changed=True, + ) + for request in work_type_requests: + work_info = request.employee_id.employee_work_info + # updating employee work information's work type to previous work type + work_info.work_type_id = request.previous_work_type_id + work_info.save() + # making the instance is in-active + request.is_active = False + request.save() + bot = User.objects.filter(username="Horilla Bot").first() + if bot is not None: + employee = request.employee_id + notify.send( + bot, + recipient=employee.employee_user_id, + verb="Work type changes notification, Requested date expired.", + verb_ar="إعلام بتغيير نوع العمل ، انتهاء صلاحية التاريخ المطلوب.", + verb_de="Benachrichtigung über Änderungen des Arbeitstyps, angefordertes Datum abgelaufen.", + verb_es="Notificación de cambios de tipo de trabajo, fecha solicitada vencida.", + verb_fr="Notification de changement de type de travail, la date demandée a expiré.", + icon="swap-horizontal", + redirect=reverse("employee-profile"), + ) + return + + +def recurring_holiday(): + from .models import Holidays + + recurring_holidays = Holidays.objects.filter(recurring=True) + today = datetime.now() + # Looping through all recurring holiday + for recurring_holiday in recurring_holidays: + start_date = recurring_holiday.start_date + end_date = recurring_holiday.end_date + new_start_date = date(start_date.year + 1, start_date.month, start_date.day) + new_end_date = date(end_date.year + 1, end_date.month, end_date.day) + # Checking that end date is not none + if end_date is None: + # checking if that start date is day before today + if start_date == (today - timedelta(days=1)).date(): + recurring_holiday.start_date = new_start_date + elif end_date == (today - timedelta(days=1)).date(): + recurring_holiday.start_date = new_start_date + recurring_holiday.end_date = new_end_date + recurring_holiday.save() + + +if not any( + cmd in sys.argv + for cmd in ["makemigrations", "migrate", "compilemessages", "flush", "shell"] +): + scheduler = BackgroundScheduler() + + # Add jobs with next_run_time set to the end of the previous job + try: + scheduler.add_job(rotate_shift, "interval", hours=4, id="job1") + except: + pass + + try: + scheduler.add_job( + rotate_work_type, + "interval", + hours=4, + id="job2", + ) + except: + pass + + try: + scheduler.add_job( + undo_shift, + "interval", + hours=4, + id="job3", + ) + except: + pass + + try: + scheduler.add_job( + switch_shift, + "interval", + hours=4, + id="job4", + ) + except: + pass + + try: + scheduler.add_job( + undo_work_type, + "interval", + hours=4, + id="job6", + ) + except: + pass + + try: + scheduler.add_job( + switch_work_type, + "interval", + hours=4, + id="job5", + ) + except: + pass + + scheduler.add_job(recurring_holiday, "interval", hours=4) + scheduler.start() diff --git a/base/signals.py b/base/signals.py new file mode 100644 index 0000000..66add5f --- /dev/null +++ b/base/signals.py @@ -0,0 +1,242 @@ +import logging +import os +import time +from datetime import datetime + +from django.apps import apps +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.signals import user_login_failed +from django.db.models import Max, Q +from django.db.models.signals import m2m_changed, post_migrate, post_save +from django.dispatch import receiver +from django.http import Http404 +from django.shortcuts import redirect, render + +from base.models import Announcement, PenaltyAccounts +from horilla.methods import get_horilla_model_class + + +@receiver(post_save, sender=PenaltyAccounts) +def create_deduction_cutleave_from_penalty(sender, instance, created, **kwargs): + """ + This is post save method, used to create deduction and cut available leave days + """ + # only work when creating + if created: + penalty_amount = instance.penalty_amount + if apps.is_installed("payroll") and penalty_amount: + Deduction = get_horilla_model_class(app_label="payroll", model="deduction") + penalty = Deduction() + if instance.late_early_id: + penalty.title = f"{instance.late_early_id.get_type_display()} penalty" + penalty.one_time_date = ( + instance.late_early_id.attendance_id.attendance_date + ) + elif instance.leave_request_id: + penalty.title = f"Leave penalty {instance.leave_request_id.end_date}" + penalty.one_time_date = instance.leave_request_id.end_date + else: + penalty.title = f"Penalty on {datetime.today()}" + penalty.one_time_date = datetime.today() + penalty.include_active_employees = False + penalty.is_fixed = True + penalty.amount = instance.penalty_amount + penalty.only_show_under_employee = True + penalty.save() + penalty.include_active_employees = False + penalty.specific_employees.add(instance.employee_id) + penalty.save() + + if ( + apps.is_installed("leave") + and instance.leave_type_id + and instance.minus_leaves + ): + available = instance.employee_id.available_leave.filter( + leave_type_id=instance.leave_type_id + ).first() + unit = round(instance.minus_leaves * 2) / 2 + if not instance.deduct_from_carry_forward: + available.available_days = max(0, (available.available_days - unit)) + else: + available.carryforward_days = max( + 0, (available.carryforward_days - unit) + ) + + available.save() + + +# @receiver(post_migrate) +def clean_work_records(sender, **kwargs): + if sender.label not in ["attendance"]: + return + from attendance.models import WorkRecords + + latest_records = ( + WorkRecords.objects.exclude(work_record_type="DFT") + .values("employee_id", "date") + .annotate(latest_id=Max("id")) + ) + + # Delete all but the latest WorkRecord + deleted_count = 0 + for record in latest_records: + deleted_count += ( + WorkRecords.objects.filter( + employee_id=record["employee_id"], date=record["date"] + ) + .exclude(id=record["latest_id"]) + .delete()[0] + ) + + +@receiver(m2m_changed, sender=Announcement.employees.through) +def filtered_employees(sender, instance, action, **kwargs): + """ + filtered employees + """ + if action not in ["post_add", "post_remove", "post_clear"]: + return # Only run after M2M changes + employee_ids = list(instance.employees.values_list("id", flat=True)) + department_ids = list(instance.department.values_list("id", flat=True)) + job_position_ids = list(instance.job_position.values_list("id", flat=True)) + + employees = instance.model_employee.objects.filter( + Q(id__in=employee_ids) + | Q(employee_work_info__department_id__in=department_ids) + | Q(employee_work_info__job_position_id__in=job_position_ids) + ) + + instance.filtered_employees.set(employees) + + +# Logger setup +logger = logging.getLogger("django.security") + +# Create a global dictionary to track login attempts and ban time per session +failed_attempts = {} +ban_time = {} + +FAIL2BAN_LOG_ENABLED = os.path.exists( + "security.log" +) # Checking that any file is created for the details of the wrong logins. +# The file will be created only if you set the LOGGING in your settings.py + + +@receiver(user_login_failed) +def log_login_failed(sender, credentials, request, **kwargs): + """ + To ban the IP of user that enter wrong credentials for multiple times + you should add this section in your settings.py file. And also it creates the security file for deatils of wrong logins. + + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'security_file': { + 'level': 'WARNING', + 'class': 'logging.FileHandler', + 'filename': '/var/log/django/security.log', # File Path for view the log details. + # Give the same path to the section FAIL2BAN_LOG_ENABLED = os.path.exists('security.log') in signals.py in Base. + }, + }, + 'loggers': { + 'django.security': { + 'handlers': ['security_file'], + 'level': 'WARNING', + 'propagate': False, + }, + }, + } + + # This section is for giving the maxtry and bantime + + FAIL2BAN_MAX_RETRY = 3 # Same as maxretry in jail.local + FAIL2BAN_BAN_TIME = 300 # Same as bantime in jail.local (in seconds) + + """ + + # Checking that the file is created or not to initiate the ban functions. + if not FAIL2BAN_LOG_ENABLED: + return + + max_attempts = getattr(settings, "FAIL2BAN_MAX_RETRY", 3) + ban_duration = getattr(settings, "FAIL2BAN_BAN_TIME", 300) + + username = credentials.get("username", "unknown") + ip = request.META.get("REMOTE_ADDR", "unknown") + session_key = ( + request.session.session_key or request.session._get_or_create_session_key() + ) + + # Check if currently banned + if session_key in ban_time and ban_time[session_key] > time.time(): + banned_until = time.strftime("%H:%M", time.localtime(ban_time[session_key])) + messages.info( + request, f"You are banned until {banned_until}. Please try again later." + ) + return redirect("/") + + # If ban expired, reset counters + if session_key in ban_time and ban_time[session_key] <= time.time(): + del ban_time[session_key] + if session_key in failed_attempts: + del failed_attempts[session_key] + + # Initialize tracking if needed + if session_key not in failed_attempts: + failed_attempts[session_key] = 0 + + failed_attempts[session_key] += 1 + attempts_left = max_attempts - failed_attempts[session_key] + + logger.warning(f"Invalid login attempt for user '{username}' from {ip}") + + if failed_attempts[session_key] >= max_attempts: + ban_time[session_key] = time.time() + ban_duration + messages.info( + request, + f"You have been banned for {ban_duration // 60} minutes due to multiple failed login attempts.", + ) + return redirect("/") + + messages.info( + request, + f"You have {attempts_left} login attempt(s) left before a temporary ban.", + ) + return redirect("login") + + +class Fail2BanMiddleware: + """ + Middleware to force password change for new employees. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + session_key = request.session.session_key + if not session_key: + request.session.create() + + # Check ban and enforce it + if session_key in ban_time and ban_time[session_key] > time.time(): + banned_until = time.strftime("%H:%M", time.localtime(ban_time[session_key])) + messages.info( + request, f"You are banned until {banned_until}. Please try again later." + ) + return render(request, "403.html") + + # If ban expired, clear counters + if session_key in ban_time and ban_time[session_key] <= time.time(): + del ban_time[session_key] + if session_key in failed_attempts: + del failed_attempts[session_key] + + return self.get_response(request) + + +settings.MIDDLEWARE.append("base.signals.Fail2BanMiddleware") diff --git a/base/tests.py b/base/tests.py new file mode 100644 index 0000000..bccdb2f --- /dev/null +++ b/base/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/base/threading.py b/base/threading.py new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..8bd3cfb96947a08ee43646068b9143b25de674e85d7aa4785fccc12171994ad3 GIT binary patch literal 1024 ScmZQz7zLvtFd70QH3R?z00031 literal 0 HcmV?d00001 diff --git a/base/translator.py b/base/translator.py new file mode 100644 index 0000000..1058f6f --- /dev/null +++ b/base/translator.py @@ -0,0 +1,364 @@ +from django.utils.translation import gettext as _ + +_("monday"), +_("tuesday"), +_("wednesday"), +_("thursday"), +_("friday"), +_("saturday"), +_("sunday"), +_("after"), +_("weekly"), +_("monthly"), +_("Employee First Name"), +_("Employee Last Name"), +_("Bank Code #1"), +_("Bank Code #2"), +_("RECRUITMENT"), +_("ONBOARDING"), +_("EMPLOYEE"), +_("PAYROLL"), +_("ATTENDANCE"), +_("LEAVE"), +_("ASSET"), +_("Your asset request approved!."), +_("Your asset request rejected!."), +_("You are added to rotating work type"), +_("You are added to rotating shift"), +_("Your work type request has been canceled."), +_("Your work type request has been approved."), +_("Your work type request has been deleted."), +_("Your shift request has been canceled."), +_("Your shift request has been approved."), +_("Your shift request has been deleted."), +_("Your work details has been updated."), +_("You have a new leave request to validate."), +_("New leave type is assigned to you"), +_("Your Leave request has been cancelled"), +_("Your Leave request has been approved"), +_("You are chosen as onboarding stage manager"), +_("You are chosen as onboarding task manager"), +_("You got an OKR!."), +_("You have received feedback!"), +_("You have been assigned as a manager in a feedback!"), +_("You have been assigned as a subordinate in a feedback!"), +_("You have been assigned as a colleague in a feedback!"), +_("You are chosen as one of recruitment manager"), +_("Your attendance for the date "), +_(" is validated"), +_("Select"), +_("January"), +_("February"), +_("March"), +_("April"), +_("May"), +_("June"), +_("July"), +_("August"), +_("September"), +_("October"), +_("November"), +_("December"), +_("One time date"), +_("Is condition based"), +_("Is taxable"), +_("Is fixed"), +_("Value"), +_("If choice"), +_("Is tax"), +_("If amount"), +_("If condition"), +_("Employer rate"), +_("Contract name"), +_("Contract start date"), +_("Contract end date"), +_("Wage type"), +_("Calculate daily leave amount"), +_("Deduction for one leave amount"), +_("Deduct leave from basic pay"), +_("Job role"), +_("Work type"), +_("Pay frequency"), +_("Filing status"), +_("Contract status"), +_("Contract document"), +_("Is tax"), +_("Update compensation"), +_("Is pretax"), +_("DASHBOARD"), +_("SHIFT REQUESTS"), +_("WORK TYPE REQUESTS"), +_("ATTENDANCE"), +_("ASSET"), +_("Single"), +_("Married"), +_("Divorced"), +_("Description"), +_("Rotate every weekend"), +_("Rotate every"), +_("Request description"), +_("Attendance validated"), +_("Is validate request"), +_("Is validate request approved"), +_("Reporting Manager"), +_("Employment Type"), +_("Jan"), +_("Feb"), +_("Mar"), +_("Apr"), +_("May"), +_("Jun"), +_("Jul"), +_("Aug"), +_("Sep"), +_("Oct"), +_("Nov"), +_("Dec"), +_("Additional info"), +_("Schedule date"), +_("Is active"), +_("End date"), +_("Recruitment managers"), +_("Stage managers"), +_("Stage type"), +_("Scheduled from"), +_("Scheduled till"), +_("Start from"), +_("End till"), +_("Employee first name"), +_("Employee last name"), +_("Reporting manager"), +_("Requested date"), +_("Previous shift"), +_("Gte"), +_("Lte"), +_("Previous work type"), +_("Current shift"), +_("Rotating shift"), +_("Next change date"), +_("Next shift"), +_("Current work type"), +_("Next work type"), +_("Start date from"), +_("Start date till"), +_("End date from"), +_("End date till"), +_("Location"), +_("Attendance clock in"), +_("Attendance clock out"), +_("Attendance overtime approve"), +_("Hour account"), +_("Clock out date"), +_("Clock in date"), +_("Shift day"), +_("Attendance date from"), +_("In from"), +_("Out from"), +_("Attendance date till"), +_("Out from"), +_("Out till"), +_("In till"), +_("Leave type"), +_("From date"), +_("To date"), +_("Assigned date"), +_("Based on week"), +_("Based on week day"), +_("Emp obj"), +_("Updated at"), +_("Created at"), +_("Created at date range"), +_("Review cycle"), +_("Asset list"), +_("Query"), +_("Asset category name"), +_("Asset category description"), +_("Asset name"), +_("Asset tracking"), +_("Asset purchase date"), +_("Asset purchase cost"), +_("Asset lot number"), +_("Asset category"), +_("Asset status"), +_("True"), +_("False"), +_("Onboarding Portal S…"), +_("Employee work information"), +_("Rotating work type assign"), +_("Employee shift schedule"), +_("Rotating shift assign"), +_("Onboarding portal"), +_("Start date breakdown"), +_("End date breakdown"), +_("Payment"), +_("dashboard"), +_("pipeline"), +_("recruitment-survey-question-template-view"), +_("candidate-view"), +_("recruitment-view"), +_("stage-view"), +_("view-onboarding-dashboard"), +_("onboarding-view"), +_("candidates-view"), +_("employee-profile"), +_("employee-view"), +_("shift-request-view"), +_("work-type-request-view"), +_("rotating-shift-assign"), +_("rotating-work-type-assign"), +_("view-payroll-dashboard"), +_("view-contract"), +_("view-allowance"), +_("view-deduction"), +_("view-payslip"), +_("filing-status-view"), +_("attendance-view"), +_("request-attendance-view"), +_("attendance-overtime-view"), +_("attendance-activity-view"), +_("late-come-early-out-view"), +_("view-my-attendance"), +_("leave-dashboard"), +_("leave-employee-dashboard"), +_("user-leave"), +_("user-request-view"), +_("type-view"), +_("assign-view"), +_("request-view"), +_("holiday-view"), +_("company-leave-view"), +_("dashboard-view"), +_("objective-list-view"), +_("feedback-view"), +_("period-view"), +_("question-template-view"), +_("asset-category-view"), +_("asset-request-allocation-view"), +_("recruitment"), +_("update-contract"), +_("update-allowance"), +_("update-deduction"), +_("type-update"), +_("type-creation"), +_("asset-batch-view"), +_("create-deduction"), +_("create-allowance"), +_("update-allowance"), +_("update-deduction"), +_("pms"), +_("asset"), +_("leave"), +_("attendance"), +_("payroll"), +_("employee"), +_("onboarding"), +_("recruitment"), +_("settings"), +_("department-view"), +_("job-position-view"), +_("job-role-view"), +_("work-type-view"), +_("rotating-work-type-view"), +_("employee-type-view"), +_("employee-shift-view"), +_("employee-shift-schedule-view"), +_("rotating-shift-view"), +_("attendance-settings-view"), +_("user-group-view"), +_("company-view"), +_("employee-permission-assign"), +_("currency"), +_("leave-allocation-request-view"), +_("employee-view-update"), +_("employee-bulk-update"), +_("not_set"), +_("objective-creation"), +_("feedback-creation"), +_("helpdesk"), +_("faq-category-view"), +_("faq-view"), +_("ticket-view"), +_("ticket-detail"), +_("ticket-type-view"), +_("tag-view"), +_("mail-server-conf"), +_("configuration"), +_("multiple-approval-condition"), +_("skill-zone-view"), +_("view-mail-templates"), +_("view-loan"), +_("view-reimbursement"), +_("department-manager-view"), +_("date-settings"), +_("reporting_manager"), +_("department"), +_("job_position"), +_("job_role"), +_("shift"), +_("work_type"), +_("company"), +_("employee-create-personal-info"), +_("offboarding"), +_("offboarding-pipeline"), +_("pagination-settings-view"), +_("organisation-chart"), +_("document-request-view"), +_("disciplinary-actions"), +_("view-policies"), +_("resignation-requests-view"), +_("action-type"), +_("general-settings"), +_("candidate-update"), +_("create-payslip"), +_("work-records"), +_("edit-profile"), +_("candidate-reject-reasons"), +_("employee-tag-view"), +_("grace-settings-view"), +_("helpdesk-tag-view"), +_("feedback-answer-view"), +_("requested"), +_("approved"), +_("cancelled"), +_("rejected"), +_("true"), +_("false"), +_("candidate-create"), +_("compensatory-leave-settings-view"), +_("view-compensatory-leave"), +_("interview-view"), +_("view-meetings"), +_("view-key-result"), +_("asset-history"), +_("restrict-view"), +_("auto-payslip-settings-view"), +_("bonus-point-setting"), +_("employee-past-leave-restriction"), +_("track-late-come-early-out"), +_("enable-biometric-attendance"), +_("allowed-ips"), +_("self-tracking-feature"), +_("candidate-reject-reasons"), +_("skills-view"), +_("employee-bonus-point"), +_("mail-automations"), +_("check-in-check-out-setting"), +_("user-accessibility"), +_("project"), +_("project-dashboard-view"), +_("project-view"), +_("task-view"), +_("task-all"), +_("view-time-sheet"), +_("backup"), +_("gdrive"), +_("horilla-theme"), +_("color-settings"), +_("report"), +_("recruitment-report"), +_("employee-report"), +_("attendance-report"), +_("leave-report"), +_("payroll-report"), +_("asset-report"), +_("pms-report"), diff --git a/base/urls.py b/base/urls.py new file mode 100644 index 0000000..e167a61 --- /dev/null +++ b/base/urls.py @@ -0,0 +1,1077 @@ +from django.contrib.auth.models import Group +from django.urls import path, re_path +from django.utils.translation import gettext_lazy as _ + +from base import announcement, request_and_approve, views +from base.forms import ( + HolidayForm, + MailTemplateForm, + RotatingShiftAssignForm, + RotatingShiftForm, + RotatingWorkTypeAssignForm, + RotatingWorkTypeForm, + ShiftRequestForm, + WorkTypeRequestForm, +) +from base.models import ( + Company, + Department, + EmployeeShift, + EmployeeShiftSchedule, + EmployeeType, + Holidays, + HorillaMailTemplate, + JobPosition, + JobRole, + RotatingShift, + RotatingShiftAssign, + RotatingWorkType, + RotatingWorkTypeAssign, + ShiftRequest, + Tags, + WorkType, + WorkTypeRequest, +) +from horilla_audit.models import AuditTag + +urlpatterns = [ + path("", views.home, name="home-page"), + path("initialize-database", views.initialize_database, name="initialize-database"), + path("load-demo-database", views.load_demo_database, name="load-demo-database"), + path( + "initialize-database-user", + views.initialize_database_user, + name="initialize-database-user", + ), + path( + "initialize-database-company", + views.initialize_database_company, + name="initialize-database-company", + ), + path( + "initialize-database-department", + views.initialize_database_department, + name="initialize-database-department", + ), + path( + "initialize-department-edit/", + views.initialize_department_edit, + name="initialize-department-edit", + ), + path( + "initialize-department-delete/", + views.initialize_department_delete, + name="initialize-department-delete", + ), + path( + "initialize-database-job-position", + views.initialize_database_job_position, + name="initialize-database-job-position", + ), + path( + "initialize-job-position-edit/", + views.initialize_job_position_edit, + name="initialize-job-position-edit", + ), + path( + "initialize-job-position-delete/", + views.initialize_job_position_delete, + name="initialize-job-position-delete", + ), + path("404", views.custom404, name="404"), + path("login/", views.login_user, name="login"), + path( + "forgot-password", + views.HorillaPasswordResetView.as_view(), + name="forgot-password", + ), + path( + "employee-reset-password", + views.EmployeePasswordResetView.as_view(), + name="employee-reset-password", + ), + path("reset-send-success", views.reset_send_success, name="reset-send-success"), + path("change-password", views.change_password, name="change-password"), + path("change-username", views.change_username, name="change-username"), + path("two-factor", views.two_factor_auth, name="two-factor"), + path("send-otp", views.send_otp, name="send-otp"), + path("logout", views.logout_user, name="logout"), + path("settings", views.common_settings, name="settings"), + path( + "settings/user-group-create/", views.user_group_table, name="user-group-create" + ), + path("settings/user-group-view/", views.user_group, name="user-group-view"), + path( + "settings/user-group-search/", views.user_group_search, name="user-group-search" + ), + path( + "user-group-delete//", + views.object_delete, + name="user-group-delete", + kwargs={"model": Group, "redirect": "user-group-view"}, + ), + path( + "group-permission-remove///", + views.user_group_permission_remove, + name="group-permission-remove", + ), + path( + "user-group-assign-view", views.group_assign_view, name="user-group-assign-view" + ), + path("settings/user-group-assign/", views.group_assign, name="user-group-assign"), + path( + "group-remove-user///", + views.group_remove_user, + name="group-remove-user", + ), + path( + "settings/employee-permission-assign/", + views.employee_permission_assign, + name="employee-permission-assign", + ), + path( + "employee-permission-search", + views.employee_permission_search, + name="permission-search", + ), + path( + "update-user-permission", + views.update_permission, + name="update-user-permission", + ), + path( + "update-group-permission", + views.update_group_permission, + name="update-group-permission", + ), + path( + "permission-table", + views.permission_table, + name="permission-table", + ), + path("settings/mail-server-conf/", views.mail_server_conf, name="mail-server-conf"), + path( + "settings/mail-server-create-update/", + views.mail_server_create_or_update, + name="mail-server-create-update", + ), + path( + "settings/mail-server-test-email/", + views.mail_server_test_email, + name="mail-server-test-email", + ), + path("mail-server-delete", views.mail_server_delete, name="mail-server-delete"), + path( + "replace-primary-mail", views.replace_primary_mail, name="replace-primary-mail" + ), + path( + "configuration/view-mail-templates/", + views.view_mail_templates, + name="view-mail-templates", + ), + path( + "view-mail-template//", + views.view_mail_template, + name="view-mail-template", + ), + path( + "create-mail-template/", + views.create_mail_templates, + name="create-mail-template", + ), + path( + "duplicate-mail-template//", + views.object_duplicate, + name="duplicate-mail-template", + kwargs={ + "model": HorillaMailTemplate, + "form": MailTemplateForm, + "template": "mail/htmx/form.html", + }, + ), + path( + "delete-mail-template/", + views.delete_mail_templates, + name="delete-mail-template", + ), + path("settings/company-create/", views.company_create, name="company-create"), + path("settings/company-view/", views.company_view, name="company-view"), + path( + "settings/company-update//", + views.company_update, + name="company-update", + kwargs={"model": Company}, + ), + path( + "settings/company-delete//", + views.object_delete, + name="company-delete", + kwargs={"model": Company, "redirect": "/settings/company-view"}, + ), + path("settings/department-view/", views.department_view, name="department-view"), + path( + "settings/department-creation/", + views.department_create, + name="department-creation", + ), + path( + "settings/department-update//", + views.department_update, + name="department-update", + kwargs={"model": Department}, + ), + path( + "department-delete//", + views.object_delete, + name="department-delete", + kwargs={ + "model": Department, + "HttpResponse": True, + }, + ), + path( + "settings/job-position-creation/", + views.job_position_creation, + name="job-position-creation", + ), + path( + "settings/job-position-view/", + views.job_position, + name="job-position-view", + ), + path( + "settings/job-position-update//", + views.job_position_update, + name="job-position-update", + kwargs={"model": JobPosition}, + ), + path( + "job-position-delete//", + views.object_delete, + name="job-position-delete", + kwargs={ + "model": JobPosition, + "HttpResponse": True, + }, + ), + path("settings/job-role-create/", views.job_role_create, name="job-role-create"), + path("settings/job-role-view/", views.job_role_view, name="job-role-view"), + path( + "settings/job-role-update//", + views.job_role_update, + name="job-role-update", + kwargs={"model": JobRole}, + ), + path( + "job-role-delete//", + views.object_delete, + name="job-role-delete", + kwargs={ + "model": JobRole, + "HttpResponse": True, + }, + ), + path("settings/work-type-view/", views.work_type_view, name="work-type-view"), + path("settings/work-type-create/", views.work_type_create, name="work-type-create"), + path( + "settings/work-type-update//", + views.work_type_update, + name="work-type-update", + kwargs={"model": WorkType}, + ), + path( + "work-type-delete//", + views.object_delete, + name="work-type-delete", + kwargs={ + "model": WorkType, + "HttpResponse": True, + }, + ), + path( + "add-remove-work-type-fields", + views.add_remove_dynamic_fields, + name="add-remove-work-type-fields", + kwargs={ + "model": WorkType, + "form_class": RotatingWorkTypeForm, + "template": "base/rotating_work_type/htmx/add_more_work_type_fields.html", + "empty_label": _("---Choose Work Type---"), + "field_name_pre": "work_type", + }, + ), + path( + "settings/rotating-work-type-create/", + views.rotating_work_type_create, + name="rotating-work-type-create", + ), + path( + "settings/rotating-work-type-view/", + views.rotating_work_type_view, + name="rotating-work-type-view", + ), + path( + "settings/rotating-work-type-update//", + views.rotating_work_type_update, + name="rotating-work-type-update", + kwargs={"model": RotatingWorkType}, + ), + path( + "rotating-work-type-delete//", + views.object_delete, + name="rotating-work-type-delete", + kwargs={ + "model": RotatingWorkType, + "HttpResponse": True, + }, + ), + path( + "employee/rotating-work-type-assign/", + views.rotating_work_type_assign, + name="rotating-work-type-assign", + ), + path( + "rotating-work-type-assign-add", + views.rotating_work_type_assign_add, + name="rotating-work-type-assign-add", + ), + path( + "rotating-work-type-assign-view", + views.rotating_work_type_assign_view, + name="rotating-work-type-assign-view", + ), + path( + "rotating-work-type-assign-export", + views.rotating_work_type_assign_export, + name="rotating-work-type-assign-export", + ), + path( + "settings/rotating-work-type-assign-update//", + views.rotating_work_type_assign_update, + name="rotating-work-type-assign-update", + ), + path( + "rotating-work-type-assign-duplicate//", + views.object_duplicate, + name="rotating-work-type-assign-duplicate", + kwargs={ + "model": RotatingWorkTypeAssign, + "form": RotatingWorkTypeAssignForm, + "template": "base/rotating_work_type/htmx/rotating_work_type_assign_form.html", + }, + ), + path( + "rotating-work-type-assign-archive//", + views.rotating_work_type_assign_archive, + name="rotating-work-type-assign-archive", + ), + path( + "rotating-work-type-assign-bulk-archive", + views.rotating_work_type_assign_bulk_archive, + name="rotating-shift-work-type-bulk-archive", + ), + path( + "rotating-work-type-assign-bulk-delete", + views.rotating_work_type_assign_bulk_delete, + name="rotating-shift-work-type-bulk-delete", + ), + path( + "rotating-work-type-assign-delete//", + views.rotating_work_type_assign_delete, + name="rotating-work-type-assign-delete", + ), + path( + "settings/employee-type-view/", + views.employee_type_view, + name="employee-type-view", + ), + path( + "settings/employee-type-create/", + views.employee_type_create, + name="employee-type-create", + ), + path( + "settings/employee-type-update//", + views.employee_type_update, + name="employee-type-update", + kwargs={"model": EmployeeType}, + ), + path( + "employee-type-delete//", + views.object_delete, + name="employee-type-delete", + kwargs={ + "model": EmployeeType, + "HttpResponse": True, + }, + ), + path( + "settings/employee-shift-view/", + views.employee_shift_view, + name="employee-shift-view", + ), + path( + "settings/employee-shift-create/", + views.employee_shift_create, + name="employee-shift-create", + ), + path( + "settings/employee-shift-update//", + views.employee_shift_update, + name="employee-shift-update", + kwargs={"model": EmployeeShift}, + ), + path( + "employee-shift-delete//", + views.object_delete, + name="employee-shift-delete", + kwargs={ + "model": EmployeeShift, + "HttpResponse": True, + }, + ), + path( + "settings/employee-shift-schedule-view/", + views.employee_shift_schedule_view, + name="employee-shift-schedule-view", + ), + path( + "settings/employee-shift-schedule-create/", + views.employee_shift_schedule_create, + name="employee-shift-schedule-create", + ), + path( + "settings/employee-shift-schedule-update//", + views.employee_shift_schedule_update, + name="employee-shift-schedule-update", + kwargs={"model": EmployeeShiftSchedule}, + ), + path( + "employee-shift-schedule-delete//", + views.object_delete, + name="employee-shift-schedule-delete", + kwargs={ + "model": EmployeeShiftSchedule, + "HttpResponse": True, + }, + ), + path( + "settings/rotating-shift-create/", + views.rotating_shift_create, + name="rotating-shift-create", + ), + path( + "add-remove-shift-fields", + views.add_remove_dynamic_fields, + name="add-remove-shift-fields", + kwargs={ + "model": EmployeeShift, + "form_class": RotatingShiftForm, + "template": "base/rotating_shift/htmx/add_more_shift_fields.html", + "empty_label": _("---Choose Shift---"), + "field_name_pre": "shift", + }, + ), + path( + "settings/rotating-shift-view/", + views.rotating_shift_view, + name="rotating-shift-view", + ), + path( + "settings/rotating-shift-update//", + views.rotating_shift_update, + name="rotating-shift-update", + kwargs={"model": RotatingShift}, + ), + path( + "rotating-shift-delete//", + views.object_delete, + name="rotating-shift-delete", + kwargs={ + "model": RotatingShift, + "redirect": "/settings/rotating-shift-view", + }, + ), + path( + "employee/rotating-shift-assign/", + views.rotating_shift_assign, + name="rotating-shift-assign", + ), + path( + "rotating-shift-assign-add", + views.rotating_shift_assign_add, + name="rotating-shift-assign-add", + ), + path( + "rotating-shift-assign-view", + views.rotating_shift_assign_view, + name="rotating-shift-assign-view", + ), + path( + "rotating-shift-assign-info-export", + views.rotating_shift_assign_export, + name="rotating-shift-assign-info-export", + ), + path( + "rotating-shift-assign-info-import", + views.rotating_shift_assign_import, + name="rotating-shift-assign-info-import", + ), + path( + "settings/rotating-shift-assign-update//", + views.rotating_shift_assign_update, + name="rotating-shift-assign-update", + ), + path( + "rotating-shift-assign-duplicate//", + views.object_duplicate, + name="rotating-shift-assign-duplicate", + kwargs={ + "model": RotatingShiftAssign, + "form": RotatingShiftAssignForm, + "template": "base/rotating_shift/htmx/rotating_shift_assign_form.html", + }, + ), + path( + "rotating-shift-assign-archive//", + views.rotating_shift_assign_archive, + name="rotating-shift-assign-archive", + ), + path( + "rotating-shift-assign-bulk-archive", + views.rotating_shift_assign_bulk_archive, + name="rotating-shift-assign-bulk-archive", + ), + path( + "rotating-shift-assign-bulk-delete", + views.rotating_shift_assign_bulk_delete, + name="rotating-shift-assign-bulk-delete", + ), + path( + "rotating-shift-assign-delete//", + views.rotating_shift_assign_delete, + name="rotating-shift-assign-delete", + ), + path("work-type-request", views.work_type_request, name="work-type-request"), + path( + "work-type-request-duplicate//", + views.object_duplicate, + name="work-type-request-duplicate", + kwargs={ + "model": WorkTypeRequest, + "form": WorkTypeRequestForm, + "template": "work_type_request/request_form.html", + }, + ), + path( + "employee/work-type-request-view/", + views.work_type_request_view, + name="work-type-request-view", + ), + path( + "work-type-request-info-export", + views.work_type_request_export, + name="work-type-request-info-export", + ), + path( + "work-type-request-search", + views.work_type_request_search, + name="work-type-request-search", + ), + path( + "work-type-request-cancel//", + views.work_type_request_cancel, + name="work-type-request-cancel", + ), + path( + "work-type-request-bulk-cancel", + views.work_type_request_bulk_cancel, + name="work-type-request-bulk-cancel", + ), + path( + "work-type-request-approve//", + views.work_type_request_approve, + name="work-type-request-approve", + ), + path( + "work-type-request-bulk-approve", + views.work_type_request_bulk_approve, + name="work-type-request-bulk-approve", + ), + path( + "work-type-request-update//", + views.work_type_request_update, + name="work-type-request-update", + ), + path( + "work-type-request-delete//", + views.work_type_request_delete, + name="work-type-request-delete", + ), + path( + "work-type-request-single-view//", + views.work_type_request_single_view, + name="work-type-request-single-view", + ), + path( + "work-type-request-bulk-delete", + views.work_type_request_bulk_delete, + name="work-type-request-bulk-delete", + ), + path("shift-request", views.shift_request, name="shift-request"), + path( + "shift-request-duplicate//", + views.object_duplicate, + name="shift-request-duplicate", + kwargs={ + "model": ShiftRequest, + "form": ShiftRequestForm, + "template": "shift_request/htmx/shift_request_create_form.html", + }, + ), + path( + "shift-request-reallocate", + views.shift_request_allocation, + name="shift-request-reallocate", + ), + path( + "update-employee-allocation", + views.update_employee_allocation, + name="update-employee-allocation", + ), + path( + "employee/shift-request-view/", + views.shift_request_view, + name="shift-request-view", + ), + path( + "shift-request-info-export", + views.shift_request_export, + name="shift-request-info-export", + ), + path( + "shift-request-search", views.shift_request_search, name="shift-request-search" + ), + path( + "shift-request-details//", + views.shift_request_details, + name="shift-request-details", + ), + path( + "shift-allocation-request-details//", + views.shift_allocation_request_details, + name="shift-allocation-request-details", + ), + path( + "shift-request-update//", + views.shift_request_update, + name="shift-request-update", + ), + path( + "shift-allocation-request-update//", + views.shift_allocation_request_update, + name="shift-allocation-request-update", + ), + path( + "shift-request-cancel//", + views.shift_request_cancel, + name="shift-request-cancel", + ), + path( + "shift-allocation-request-cancel//", + views.shift_allocation_request_cancel, + name="shift-allocation-request-cancel", + ), + path( + "shift-request-bulk-cancel", + views.shift_request_bulk_cancel, + name="shift-request-bulk-cancel", + ), + path( + "shift-request-approve//", + views.shift_request_approve, + name="shift-request-approve", + ), + path( + "shift-allocation-request-approve//", + views.shift_allocation_request_approve, + name="shift-allocation-request-approve", + ), + path( + "shift-request-bulk-approve", + views.shift_request_bulk_approve, + name="shift-request-bulk-approve", + ), + path( + "shift-request-delete//", + views.shift_request_delete, + name="shift-request-delete", + ), + path( + "shift-request-bulk-delete", + views.shift_request_bulk_delete, + name="shift-request-bulk-delete", + ), + path("notifications", views.notifications, name="notifications"), + path("clear-notifications", views.clear_notification, name="clear-notifications"), + path( + "delete-all-notifications", + views.delete_all_notifications, + name="delete-all-notifications", + ), + path("read-notifications", views.read_notifications, name="read-notifications"), + path( + "mark-as-read-notification/", + views.mark_as_read_notification, + name="mark-as-read-notification", + ), + path( + "mark-as-read-notification-json/", + views.mark_as_read_notification_json, + name="mark-as-read-notification-json", + ), + path("all-notifications", views.all_notifications, name="all-notifications"), + path( + "delete-notifications//", + views.delete_notification, + name="delete-notifications", + ), + path("settings/general-settings/", views.general_settings, name="general-settings"), + path("settings/date-settings/", views.date_settings, name="date-settings"), + path("settings/save-date/", views.save_date_format, name="save_date_format"), + path("settings/get-date-format/", views.get_date_format, name="get-date-format"), + path("settings/save-time/", views.save_time_format, name="save_time_format"), + path("settings/get-time-format/", views.get_time_format, name="get-time-format"), + path( + "history-field-settings", + views.history_field_settings, + name="history-field-settings", + ), + path( + "enable-account-block-unblock", + views.enable_account_block_unblock, + name="enable-account-block-unblock", + ), + path( + "enable-profile-edit-feature", + views.enable_profile_edit_feature, + name="enable-profile-edit-feature", + ), + path( + "rwork-individual-view//", + views.rotating_work_individual_view, + name="rwork-individual-view", + ), + path( + "rshit-individual-view//", + views.rotating_shift_individual_view, + name="rshift-individual-view", + ), + path("shift-select/", views.shift_select, name="shift-select"), + path( + "shift-select-filter/", + views.shift_select_filter, + name="shift-select-filter", + ), + path("work-type-select/", views.work_type_select, name="work-type-select"), + path( + "work-type-filter/", + views.work_type_select_filter, + name="work-type-select-filter", + ), + path("r-shift-select/", views.rotating_shift_select, name="r-shift-select"), + path( + "r-shift-select-filter/", + views.rotating_shift_select_filter, + name="r-shift-select-filter", + ), + path( + "r-work-type-select/", + views.rotating_work_type_select, + name="r-work-type-select", + ), + path( + "r-work-type-filter/", + views.rotating_work_type_select_filter, + name="r-work-type-select-filter", + ), + path("settings/tag-view/", views.tag_view, name="tag-view"), + path( + "settings/helpdesk-tag-view/", views.helpdesk_tag_view, name="helpdesk-tag-view" + ), + path("tag-create", views.tag_create, name="tag-create"), + path("tag-update/", views.tag_update, name="tag-update"), + path( + "tag-delete/", + views.object_delete, + name="tag-delete", + kwargs={ + "model": Tags, + "HttpResponse": True, + }, + ), + path("audit-tag-create", views.audit_tag_create, name="audit-tag-create"), + path( + "audit-tag-update/", views.audit_tag_update, name="audit-tag-update" + ), + path( + "audit-tag-delete/", + views.object_delete, + name="audit-tag-delete", + kwargs={"model": AuditTag, "HttpResponse": True}, + ), + path( + "configuration/multiple-approval-condition", + views.multiple_approval_condition, + name="multiple-approval-condition", + ), + path( + "configuration/condition-value-fields", + views.get_condition_value_fields, + name="condition-value-fields", + ), + path( + "configuration/add-more-approval-managers", + views.add_more_approval_managers, + name="add-more-approval-managers", + ), + path( + "configuration/remove-approval-manager", + views.remove_approval_manager, + name="remove-approval-manager", + ), + path( + "configuration/hx-multiple-approval-condition", + views.hx_multiple_approval_condition, + name="hx-multiple-approval-condition", + ), + path( + "multiple-level-approval-create", + views.multiple_level_approval_create, + name="multiple-level-approval-create", + ), + path( + "multiple-level-approval-edit/", + views.multiple_level_approval_edit, + name="multiple-level-approval-edit", + ), + path( + "multiple-level-approval-delete/", + views.multiple_level_approval_delete, + name="multiple-level-approval-delete", + ), + path( + "shift-request-add-comment//", + views.create_shiftrequest_comment, + name="shift-request-add-comment", + ), + path( + "view-shift-comment//", + views.view_shift_comment, + name="view-shift-comment", + ), + path( + "delete-shift-comment-file/", + views.delete_shift_comment_file, + name="delete-shift-comment-file", + ), + path( + "view-work-type-comment//", + views.view_work_type_comment, + name="view-work-type-comment", + ), + path( + "delete-work-type-comment-file/", + views.delete_work_type_comment_file, + name="delete-work-type-comment-file", + ), + path( + "shift-request-delete-comment//", + views.delete_shiftrequest_comment, + name="shift-request-delete-comment", + ), + path( + "worktype-request-add-comment//", + views.create_worktyperequest_comment, + name="worktype-request-add-comment", + ), + path( + "worktype-request-delete-comment//", + views.delete_worktyperequest_comment, + name="worktype-request-delete-comment", + ), + path( + "dashboard-shift-request", + request_and_approve.dashboard_shift_request, + name="dashboard-shift-request", + ), + path( + "dashboard-work-type-request", + request_and_approve.dashboard_work_type_request, + name="dashboard-work-type-request", + ), + path( + "settings/pagination-settings-view/", + views.pagination_settings_view, + name="pagination-settings-view", + ), + path("settings/action-type/", views.action_type_view, name="action-type"), + path("action-type-create", views.action_type_create, name="action-type-create"), + path( + "action-type-update/", + views.action_type_update, + name="action-type-update", + ), + path( + "action-type-delete/", + views.action_type_delete, + name="action-type-delete", + ), + path( + "pagination-settings-view", + views.pagination_settings_view, + name="pagination-settings-view", + ), + path("announcement-list", announcement.announcement_list, name="announcement-list"), + path( + "create-announcement", + announcement.create_announcement, + name="create-announcement", + ), + path( + "delete-announcement/", + announcement.delete_announcement, + name="delete-announcement", + ), + path( + "update-announcement/", + announcement.update_announcement, + name="update-announcement", + ), + path( + "remove-announcement-file//", + announcement.remove_announcement_file, + name="remove-announcement-file", + ), + path( + "announcement-add-comment//", + announcement.create_announcement_comment, + name="announcement-add-comment", + ), + path( + "announcement-view-comment//", + announcement.comment_view, + name="announcement-view-comment", + ), + path( + "announcement-single-view/", + announcement.announcement_single_view, + name="announcement-single-view", + ), + path( + "announcement-single-view/", + announcement.announcement_single_view, + name="announcement-single-view", + ), + path( + "announcement-delete-comment//", + announcement.delete_announcement_comment, + name="announcement-delete-comment", + ), + path( + "announcement-viewed-by", announcement.viewed_by, name="announcement-viewed-by" + ), + path("driver-viewed", views.driver_viewed_status, name="driver-viewed"), + path( + "dashboard-components-toggle", + views.dashboard_components_toggle, + name="dashboard-components-toggle", + ), + path("employee-chart-show", views.employee_chart_show, name="employee-chart-show"), + path( + "settings/enable-biometric-attendance/", + views.enable_biometric_attendance_view, + name="enable-biometric-attendance", + ), + path( + "settings/activate-biometric-attendance", + views.activate_biometric_attendance, + name="activate-biometric-attendance", + ), + path( + "emp-workinfo-complete", + views.employee_workinfo_complete, + name="emp-workinfo-complete", + ), + path( + "get-horilla-installed-apps/", + views.get_horilla_installed_apps, + name="get-horilla-installed-apps", + ), + path("configuration/holiday-view", views.holiday_view, name="holiday-view"), + path( + "configuration/holidays-excel-template", + views.holidays_excel_template, + name="holidays-excel-template", + ), + path( + "holidays-info-import", views.holidays_info_import, name="holidays-info-import" + ), + path("holiday-info-export", views.holiday_info_export, name="holiday-info-export"), + path( + "get-upcoming-holidays", + views.get_upcoming_holidays, + name="get-upcoming-holidays", + ), + path("holiday-creation", views.holiday_creation, name="holiday-creation"), + path("holiday-update/", views.holiday_update, name="holiday-update"), + path( + "duplicate-holiday/", + views.object_duplicate, + name="duplicate-holiday", + kwargs={ + "model": Holidays, + "form": HolidayForm, + "template": "holiday/holiday_form.html", + }, + ), + path("holiday-delete/", views.holiday_delete, name="holiday-delete"), + path( + "holidays-bulk-delete", views.bulk_holiday_delete, name="holidays-bulk-delete" + ), + path("holiday-filter", views.holiday_filter, name="holiday-filter"), + path("holiday-select/", views.holiday_select, name="holiday-select"), + path( + "holiday-select-filter/", + views.holiday_select_filter, + name="holiday-select-filter", + ), + path( + "company-leave-creation", + views.company_leave_creation, + name="company-leave-creation", + ), + path( + "configuration/company-leave-view", + views.company_leave_view, + name="company-leave-view", + ), + path( + "company-leave-update/", + views.company_leave_update, + name="company-leave-update", + ), + path( + "company-leave-delete/", + views.company_leave_delete, + name="company-leave-delete", + ), + path( + "company-leave-filter", views.company_leave_filter, name="company-leave-filter" + ), + path("view-penalties", views.view_penalties, name="view-penalties"), +] + +urlpatterns.append( + re_path(r"^media/(?P.*)$", views.protected_media, name="protected_media"), +) diff --git a/base/views.py b/base/views.py new file mode 100644 index 0000000..bad2fe5 --- /dev/null +++ b/base/views.py @@ -0,0 +1,7553 @@ +""" +views.py + +This module is used to map url pattens with django views or methods +""" + +import csv +import json +import os +import threading +import uuid +from datetime import datetime, timedelta +from email.mime.image import MIMEImage +from os import path +from urllib.parse import parse_qs, unquote, urlencode, urlparse + +import pandas as pd +from dateutil import parser +from django import forms +from django.apps import apps +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.models import Group, Permission, User +from django.contrib.auth.views import PasswordResetConfirmView, PasswordResetView +from django.core.files.base import ContentFile +from django.core.mail import EmailMessage, EmailMultiAlternatives +from django.core.management import call_command +from django.db.models import ProtectedError, Q +from django.http import ( + FileResponse, + Http404, + HttpResponse, + HttpResponseRedirect, + JsonResponse, +) +from django.shortcuts import get_object_or_404, redirect, render +from django.template.loader import render_to_string +from django.urls import reverse, reverse_lazy +from django.utils import timezone +from django.utils.html import strip_tags +from django.utils.http import url_has_allowed_host_and_scheme +from django.utils.translation import gettext as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from accessibility.accessibility import ACCESSBILITY_FEATURE +from accessibility.models import DefaultAccessibility +from base.backends import ConfiguredEmailBackend +from base.decorators import ( + shift_request_change_permission, + work_type_request_change_permission, +) +from base.filters import ( + CompanyLeaveFilter, + HolidayFilter, + PenaltyFilter, + RotatingShiftAssignFilters, + RotatingShiftRequestReGroup, + RotatingWorkTypeAssignFilter, + RotatingWorkTypeRequestReGroup, + ShiftRequestFilter, + ShiftRequestReGroup, + WorkTypeRequestFilter, + WorkTypeRequestReGroup, +) +from base.forms import ( + AnnouncementExpireForm, + AssignPermission, + AssignUserGroup, + AuditTagForm, + ChangePasswordForm, + ChangeUsernameForm, + CompanyForm, + CompanyLeaveForm, + DepartmentForm, + DriverForm, + DynamicMailConfForm, + DynamicMailTestForm, + DynamicPaginationForm, + EmployeeShiftForm, + EmployeeShiftScheduleForm, + EmployeeShiftScheduleUpdateForm, + EmployeeTypeForm, + HolidayForm, + HolidaysColumnExportForm, + JobPositionForm, + JobPositionMultiForm, + JobRoleForm, + MailTemplateForm, + MultipleApproveConditionForm, + PassWordResetForm, + ResetPasswordForm, + RotatingShiftAssign, + RotatingShiftAssignExportForm, + RotatingShiftAssignForm, + RotatingShiftAssignUpdateForm, + RotatingShiftForm, + RotatingWorkTypeAssignExportForm, + RotatingWorkTypeAssignForm, + RotatingWorkTypeAssignUpdateForm, + RotatingWorkTypeForm, + ShiftAllocationForm, + ShiftRequestColumnForm, + ShiftRequestCommentForm, + ShiftRequestForm, + TagsForm, + UserGroupForm, + WorkTypeForm, + WorkTypeRequestColumnForm, + WorkTypeRequestCommentForm, + WorkTypeRequestForm, +) +from base.methods import ( + choosesubordinates, + closest_numbers, + export_data, + filtersubordinates, + filtersubordinatesemployeemodel, + format_date, + generate_colors, + generate_otp, + get_key_instances, + is_reportingmanager, + paginator_qry, + sortby, +) +from base.models import ( + WEEK_DAYS, + WEEKS, + AnnouncementExpire, + BaserequestFile, + BiometricAttendance, + Company, + CompanyLeaves, + DashboardEmployeeCharts, + Department, + DynamicEmailConfiguration, + DynamicPagination, + EmployeeShift, + EmployeeShiftSchedule, + EmployeeType, + Holidays, + HorillaMailTemplate, + JobPosition, + JobRole, + MultipleApprovalCondition, + MultipleApprovalManagers, + RotatingShift, + RotatingWorkType, + RotatingWorkTypeAssign, + ShiftRequest, + ShiftRequestComment, + Tags, + WorkType, + WorkTypeRequest, + WorkTypeRequestComment, +) +from employee.filters import EmployeeFilter +from employee.forms import ActiontypeForm, EmployeeGeneralSettingPrefixForm +from employee.models import ( + Actiontype, + DisciplinaryAction, + Employee, + EmployeeGeneralSetting, + EmployeeWorkInformation, + ProfileEditFeature, +) +from horilla import horilla_apps +from horilla.decorators import ( + delete_permission, + duplicate_permission, + hx_request_required, + login_required, + manager_can_enter, + permission_required, +) +from horilla.group_by import group_by_queryset +from horilla.horilla_settings import ( + APPS, + DB_INIT_PASSWORD, + DYNAMIC_URL_PATTERNS, + FILE_STORAGE, + NO_PERMISSION_MODALS, +) +from horilla.methods import get_horilla_model_class, remove_dynamic_url +from horilla_audit.forms import HistoryTrackingFieldsForm +from horilla_audit.models import AccountBlockUnblock, AuditTag, HistoryTrackingFields +from notifications.models import Notification +from notifications.signals import notify + + +def custom404(request): + """ + Custom 404 method + """ + return render(request, "404.html") + + +# Create your views here. +def is_reportingmanger(request, instance): + """ + If the instance have employee id field then you can use this method to know the request + user employee is the reporting manager of the instance + """ + manager = request.user.employee_get + try: + employee_work_info_manager = ( + instance.employee_id.employee_work_info.reporting_manager_id + ) + except Exception: + return HttpResponse("This Employee Dont Have any work information") + return manager == employee_work_info_manager + + +def initialize_database_condition(): + """ + Determines if the database initialization process should be triggered. + + This function checks whether there are any users in the database. If there are no users, + or if there are superusers without associated employees, it indicates that the database + needs to be initialized. + + Returns: + bool: True if the database needs to be initialized, False otherwise. + """ + init_database = not User.objects.exists() + if not init_database: + init_database = True + superusers = User.objects.filter(is_superuser=True) + for user in superusers: + if hasattr(user, "employee_get"): + init_database = False + break + return init_database + + +def load_demo_database(request): + if initialize_database_condition(): + if request.method == "POST": + if request.POST.get("load_data_password") == DB_INIT_PASSWORD: + data_files = [ + "user_data.json", + "employee_info_data.json", + "base_data.json", + "work_info_data.json", + ] + optional_apps = [ + ("attendance", "attendance_data.json"), + ("leave", "leave_data.json"), + ("asset", "asset_data.json"), + ("recruitment", "recruitment_data.json"), + ("onboarding", "onboarding_data.json"), + ("offboarding", "offboarding_data.json"), + ("pms", "pms_data.json"), + ("payroll", "payroll_data.json"), + ("payroll", "payroll_loanaccount_data.json"), + ("project", "project_data.json"), + ] + + # Add data files for installed apps + data_files += [ + file for app, file in optional_apps if apps.is_installed(app) + ] + + # Load all data files + for file in data_files: + file_path = path.join(settings.BASE_DIR, "load_data", file) + try: + call_command("loaddata", file_path) + except Exception as e: + messages.error(request, f"An error occured : {e}") + + messages.success(request, _("Database loaded successfully.")) + else: + messages.error(request, _("Database Authentication Failed")) + return redirect(home) + return redirect("/") + + +def initialize_database(request): + """ + Handles the database initialization process via a user interface. + + Parameters: + request (HttpRequest): The request object. + + Returns: + HttpResponse: The rendered HTML template or a redirect response. + """ + if initialize_database_condition(): + if request.method == "POST": + password = request._post.get("password") + if DB_INIT_PASSWORD == password: + return redirect(initialize_database_user) + else: + messages.warning( + request, + _("The password you entered is incorrect. Please try again."), + ) + return HttpResponse("") + return render(request, "initialize_database/horilla_user.html") + else: + return redirect("/") + + +@hx_request_required +def initialize_database_user(request): + """ + Handles the user creation step during database initialization. + + Parameters: + request (HttpRequest): The request object. + + Returns: + HttpResponse: The rendered HTML template for company creation or user signup. + """ + if request.method == "POST": + form_data = request.__dict__.get("_post") + username = form_data.get("username") + password = form_data.get("password") + confirm_password = form_data.get("confirm_password") + if password != confirm_password: + return render(request, "initialize_database/horilla_user_signup.html") + first_name = form_data.get("firstname") + last_name = form_data.get("lastname") + badge_id = form_data.get("badge_id") + email = form_data.get("email") + phone = form_data.get("phone") + user = User.objects.filter(username=username).first() + if user and not hasattr(user, "employee_get"): + user.delete() + user = User.objects.create_superuser( + username=username, email=email, password=password + ) + employee = Employee() + employee.employee_user_id = user + employee.badge_id = badge_id + employee.employee_first_name = first_name + employee.employee_last_name = last_name + employee.email = email + employee.phone = phone + employee.save() + user = authenticate(request, username=username, password=password) + login(request, user) + return render( + request, + "initialize_database/horilla_company.html", + {"form": CompanyForm(initial={"hq": True})}, + ) + return render(request, "initialize_database/horilla_user_signup.html") + + +@hx_request_required +def initialize_database_company(request): + """ + Handles the company creation step during database initialization. + + Parameters: + request (HttpRequest): The request object. + + Returns: + HttpResponse: The rendered HTML template for department creation or company creation. + """ + form = CompanyForm() + if request.method == "POST": + form = CompanyForm(request.POST, request.FILES) + if form.is_valid(): + company = form.save() + try: + employee = request.user.employee_get + employee.employee_work_info.company_id = company + employee.employee_work_info.save() + except: + pass + return render( + request, + "initialize_database/horilla_department.html", + {"form": DepartmentForm(initial={"company_id": company})}, + ) + return render(request, "initialize_database/horilla_company.html", {"form": form}) + + +@hx_request_required +def initialize_database_department(request): + """ + Handles the department creation step during database initialization. + + Parameters: + request (HttpRequest): The request object. + + Returns: + HttpResponse: The rendered HTML template for department creation. + """ + departments = Department.objects.all() + form = DepartmentForm(initial={"company_id": Company.objects.first()}) + if request.method == "POST": + form = DepartmentForm(request.POST) + if form.is_valid(): + company = form.cleaned_data.get("company_id") + form.save() + form = DepartmentForm(initial={"company_id": company}) + return render( + request, + "initialize_database/horilla_department_form.html", + {"form": form, "departments": departments}, + ) + + +@hx_request_required +def initialize_department_edit(request, obj_id): + """ + Handles editing of an existing department during database initialization. + + Parameters: + request (HttpRequest): The request object. + obj_id (int): The ID of the department to be edited. + + Returns: + HttpResponse: The rendered HTML template for department editing. + """ + department = Department.find(obj_id) + form = DepartmentForm(instance=department) + if request.method == "POST": + form = DepartmentForm(request.POST, instance=department) + if form.is_valid(): + company = form.cleaned_data.get("company_id") + form.save() + return render( + request, + "initialize_database/horilla_department_form.html", + { + "form": DepartmentForm(initial={"company_id": company}), + "departments": Department.objects.all(), + }, + ) + return render( + request, + "initialize_database/horilla_department_form.html", + { + "form": form, + "department": department, + "departments": Department.objects.all(), + }, + ) + + +@hx_request_required +def initialize_department_delete(request, obj_id): + """ + Handles the deletion of an existing department during database initialization. + + Parameters: + request (HttpRequest): The request object. + obj_id (int): The ID of the department to be deleted. + + Returns: + HttpResponse: A redirect response to the department creation page. + """ + department = Department.find(obj_id) + department.delete() if department else None + return redirect(initialize_database_department) + + +@hx_request_required +def initialize_database_job_position(request): + """ + Handles the job position creation step during database initialization. + + Parameters: + request (HttpRequest): The request object. + + Returns: + HttpResponse: The rendered HTML template for job position creation. + """ + company = Company.objects.first() + form = JobPositionMultiForm(initial={"company_id": company}) + if request.method == "POST": + form = JobPositionMultiForm(request.POST) + if form.is_valid(): + form.save() + form = JobPositionMultiForm(initial={"company_id": Company.objects.first()}) + return render( + request, + "initialize_database/horilla_job_position_form.html", + { + "form": form, + "job_positions": JobPosition.objects.all(), + "company": company, + }, + ) + return render( + request, + "initialize_database/horilla_job_position.html", + {"form": form, "job_positions": JobPosition.objects.all(), "company": company}, + ) + + +@hx_request_required +def initialize_job_position_edit(request, obj_id): + """ + Handles editing of an existing job position during database initialization. + + Parameters: + request (HttpRequest): The request object. + obj_id (int): The ID of the job position to be edited. + + Returns: + HttpResponse: The rendered HTML template for job position editing. + """ + company = Company.objects.first() + job_position = JobPosition.find(obj_id) + form = JobPositionForm(instance=job_position) + if request.method == "POST": + form = JobPositionForm(request.POST, instance=job_position) + if form.is_valid(): + form.save() + return render( + request, + "initialize_database/horilla_job_position_form.html", + { + "form": JobPositionMultiForm(initial={"company_id": company}), + "job_positions": JobPosition.objects.all(), + "company": company, + }, + ) + return render( + request, + "initialize_database/horilla_job_position_form.html", + { + "form": form, + "job_position": job_position, + "job_positions": JobPosition.objects.all(), + "company": company, + }, + ) + + +@hx_request_required +def initialize_job_position_delete(request, obj_id): + """ + Handles the deletion of an existing job position during database initialization. + + Parameters: + request (HttpRequest): The request object. + obj_id (int): The ID of the job position to be deleting. + + Returns: + HttpResponse: The rendered HTML template for job position creating. + """ + company = Company.objects.first() + job_position = JobPosition.find(obj_id) + job_position.delete() if job_position else None + return render( + request, + "initialize_database/horilla_job_position_form.html", + { + "form": JobPositionMultiForm( + initial={"company_id": Company.objects.first()} + ), + "job_positions": JobPosition.objects.all(), + "company": company, + }, + ) + + +def login_user(request): + """ + Handles user login and authentication. + """ + if request.method == "POST": + username = request.POST.get("username") + password = request.POST.get("password") + next_url = request.GET.get("next", "/") + query_params = request.GET.dict() + query_params.pop("next", None) + params = urlencode(query_params) + + user = authenticate(request, username=username, password=password) + + if not user: + user_object = User.objects.filter(username=username).first() + if user_object and not user_object.is_active: + messages.warning(request, _("Access Denied: Your account is blocked.")) + else: + messages.error(request, _("Invalid username or password.")) + return redirect("login") + + employee = getattr(user, "employee_get", None) + if employee is None: + messages.error( + request, + _("An employee related to this user's credentials does not exist."), + ) + return redirect("login") + if not employee.is_active: + messages.warning( + request, + _( + "This user is archived. Please contact the manager for more information." + ), + ) + return redirect("login") + + login(request, user) + + messages.success(request, _("Login successful.")) + + # Ensure `next_url` is a safe local URL + if not url_has_allowed_host_and_scheme( + next_url, allowed_hosts={request.get_host()} + ): + next_url = "/" + + if params: + next_url += f"?{params}" + return redirect(next_url) + + return render( + request, "login.html", {"initialize_database": initialize_database_condition()} + ) + + +def include_employee_instance(request, form): + """ + This method is used to include the employee instance to the form + Args: + form: django forms instance + """ + queryset = form.fields["employee_id"].queryset + employee = Employee.objects.filter(employee_user_id=request.user) + if employee.first() is not None: + if queryset.filter(id=employee.first().id).first() is None: + # queryset = queryset | employee + queryset = queryset.distinct() | employee.distinct() + form.fields["employee_id"].queryset = queryset + return form + + +def reset_send_success(request): + return render(request, "reset_send.html") + + +class HorillaPasswordResetView(PasswordResetView): + """ + Horilla View for Reset Password + """ + + template_name = "forgot_password.html" + form_class = PassWordResetForm + success_url = reverse_lazy("reset-send-success") + + def form_valid(self, form): + email_backend = ConfiguredEmailBackend() + default = "base.backends.ConfiguredEmailBackend" + is_default_backend = True + EMAIL_BACKEND = getattr(settings, "EMAIL_BACKEND", "") + if EMAIL_BACKEND and default != EMAIL_BACKEND: + is_default_backend = False + if is_default_backend and not email_backend.configuration: + messages.error(self.request, _("Primary mail server is not configured")) + return redirect("forgot-password") + + username = form.cleaned_data["email"] + user = User.objects.filter(username=username).first() + if user: + opts = { + "use_https": self.request.is_secure(), + "token_generator": self.token_generator, + "from_email": email_backend.dynamic_from_email_with_display_name, + "email_template_name": self.email_template_name, + "subject_template_name": self.subject_template_name, + "request": self.request, + "html_email_template_name": self.html_email_template_name, + "extra_email_context": self.extra_email_context, + } + form.save(**opts) + if self.request.user.is_authenticated: + messages.success( + self.request, _("Password reset link sent successfully") + ) + return HttpResponseRedirect(self.request.META.get("HTTP_REFERER", "/")) + + return redirect(reverse_lazy("reset-send-success")) + + messages.info(self.request, _("No user found with the username")) + return redirect("forgot-password") + + +class EmployeePasswordResetView(PasswordResetView): + """ + Horilla View for Employee Reset Password + """ + + template_name = "forgot_password.html" + form_class = PassWordResetForm + + def form_valid(self, form): + try: + email_backend = ConfiguredEmailBackend() + default = "base.backends.ConfiguredEmailBackend" + is_default_backend = True + EMAIL_BACKEND = getattr(settings, "EMAIL_BACKEND", "") + if EMAIL_BACKEND and default != EMAIL_BACKEND: + is_default_backend = False + if is_default_backend and not email_backend.configuration: + messages.error(self.request, _("Primary mail server is not configured")) + return HttpResponseRedirect(self.request.META.get("HTTP_REFERER", "/")) + + username = form.cleaned_data["email"] + user = User.objects.filter(username=username).first() + if user: + opts = { + "use_https": self.request.is_secure(), + "token_generator": self.token_generator, + "from_email": email_backend.dynamic_from_email_with_display_name, + "email_template_name": self.email_template_name, + "subject_template_name": self.subject_template_name, + "request": self.request, + "html_email_template_name": self.html_email_template_name, + "extra_email_context": self.extra_email_context, + } + form.save(**opts) + messages.success( + self.request, _("Password reset link sent successfully") + ) + else: + messages.error(self.request, _("No user with the given username")) + return HttpResponseRedirect(self.request.META.get("HTTP_REFERER", "/")) + + except Exception as e: + messages.error(self.request, f"Something went wrong.....") + return HttpResponseRedirect(self.request.META.get("HTTP_REFERER", "/")) + + +setattr(PasswordResetConfirmView, "template_name", "reset_password.html") +setattr(PasswordResetConfirmView, "form_class", ResetPasswordForm) +setattr(PasswordResetConfirmView, "success_url", "/") + + +@login_required +def change_password(request): + """ + Handles the password change process for a logged-in user. + + Args: + request (HttpRequest): The HTTP request object containing metadata about + the request and user. + + Returns: + HttpResponse: Renders the password change form if the request method is GET or + the form is invalid. If the form is valid and the password is changed + successfully, the page reloads with a success message. + """ + user = request.user + form = ChangePasswordForm(user=user) + if request.method == "POST": + form = ChangePasswordForm(user, request.POST) + if form.is_valid(): + new_password = form.cleaned_data["new_password"] + user.set_password(new_password) + user.save() + user = authenticate(request, username=user.username, password=new_password) + if hasattr(user, "is_new_employee"): + user.is_new_employee = False + user.save() + login(request, user) + messages.success(request, _("Password changed successfully")) + return HttpResponse("") + return render(request, "base/auth/password_change_form.html", {"form": form}) + + return render(request, "base/auth/password_change.html", {"form": form}) + + +@login_required +def change_username(request): + """ + Handles the username change process for a logged-in user. + + Args: + request (HttpRequest): The HTTP request object containing metadata about + the request and user. + + Returns: + HttpResponse: Renders the username change form if the request method is GET or + the form is invalid. If the form is valid and the password is changed + successfully, the page reloads with a success message. + """ + user = request.user + form = ChangeUsernameForm(user=user, initial={"old_username": user.username}) + if request.method == "POST": + form = ChangeUsernameForm(user, request.POST) + if form.is_valid(): + new_username = form.cleaned_data["username"] + user.username = new_username + user.save() + if hasattr(user, "is_new_employee"): + user.is_new_employee = False + user.save() + messages.success(request, _("Username changed successfully")) + return HttpResponse("") + return render(request, "base/auth/username_change_form.html", {"form": form}) + + return render(request, "base/auth/username_change.html", {"form": form}) + + +def two_factor_auth(request): + """ + function to handle two-factor authentication for users. + """ + # request.session["otp_code"] = None + try: + otp = get_otp(request) + except: + otp = None + + if request.method == "POST": + user_otp = request.POST.get("otp") + if user_otp == otp: + request.session["otp_code"] = None + request.session["otp_code_timestamp"] = None + request.session["otp_code_verified"] = True + request.session.save() + messages.success(request, "OTP verified successfully.") + return redirect("/") + elif otp is None: + messages.error(request, "OTP expired. Please request a new one.") + return render(request, "base/auth/two_factor_auth.html") + else: + messages.error(request, "Invalid OTP.") + return render(request, "base/auth/two_factor_auth.html") + + if not horilla_apps.TWO_FACTORS_AUTHENTICATION: + return redirect("/") + + if otp is None: + send_otp(request) + return render(request, "base/auth/two_factor_auth.html") + + +def send_otp(request): + """ + Function to send OTP to the user's email address. + It generates a new OTP code, stores it in the session, and sends it via email. + """ + employee = request.user.employee_get + email = employee.get_mail() + + email_backend = ConfiguredEmailBackend() + display_email_name = email_backend.dynamic_from_email_with_display_name + + otp_code = set_otp(request) + email = EmailMessage( + subject="Your OTP Code", + body=f"Your OTP code is {otp_code}", + from_email=display_email_name, + to=[email], + ) + thread = threading.Thread(target=email.send) + thread.start() + + return redirect("two-factor") + + +def set_otp(request): + """ + Function to set the OTP code in the session. + Generates a new OTP code, stores it in the session, and sets a timestamp for expiration. + """ + + otp_code = generate_otp() + request.session["otp_code"] = otp_code + request.session["otp_code_timestamp"] = timezone.now().timestamp() + request.session["otp_code_verified"] = False + request.session.save() + return otp_code + + +def get_otp(request): + """ + Function to retrieve the OTP code from the session. + Checks if the OTP code has expired (10 minutes) and clears it if so. + """ + created_at = request.session.get("otp_code_timestamp", 0) + current_time = timezone.now().timestamp() + + if current_time - created_at > 600: + request.session["otp_code"] = None + request.session["otp_code_timestamp"] = None + request.session.save() + return None + else: + return request.session.get("otp_code") + + +def logout_user(request): + """ + This method used to logout the user + """ + if request.user: + logout(request) + response = HttpResponse() + response.content = """ + + + """ + + return response + + +class Workinfo: + def __init__(self, employee) -> None: + self.employee_work_info = employee + self.employee_id = employee + self.id = employee.id + pass + + +@login_required +def home(request): + """ + This method is used to render index page + """ + + today = datetime.today() + today_weekday = today.weekday() + first_day_of_week = today - timedelta(days=today_weekday) + last_day_of_week = first_day_of_week + timedelta(days=6) + + employee_charts = DashboardEmployeeCharts.objects.get_or_create( + employee=request.user.employee_get + )[0] + + user = request.user + today = timezone.now().date() # Get today's date + is_birthday = None + + if user.employee_get.dob != None: + is_birthday = ( + user.employee_get.dob.month == today.month + and user.employee_get.dob.day == today.day + ) + + context = { + "first_day_of_week": first_day_of_week.strftime("%Y-%m-%d"), + "last_day_of_week": last_day_of_week.strftime("%Y-%m-%d"), + "charts": employee_charts.charts, + "is_birthday": is_birthday, + } + + return render(request, "index.html", context) + + +@login_required +@manager_can_enter("employee.view_employeeworkinformation") +def employee_workinfo_complete(request): + + employees_with_pending = [] + + # List of field names to focus on + fields_to_focus = [ + "job_position_id", + "department_id", + "work_type_id", + "employee_type_id", + "job_role_id", + "reporting_manager_id", + "company_id", + "location", + "email", + "mobile", + "shift_id", + "date_joining", + "contract_end_date", + "basic_salary", + "salary_hour", + ] + search = request.GET.get("search", "") + employees_workinfos = filtersubordinates( + request, + queryset=EmployeeWorkInformation.objects.filter( + employee_id__employee_first_name__icontains=search, + employee_id__is_active=True, + ), + perm="employee.view_employeeworkinformation", + ) + for employee in employees_workinfos: + completed_field_count = sum( + 1 + for field_name in fields_to_focus + if getattr(employee, field_name) is not None + ) + if completed_field_count < 15: + # Create a dictionary with employee information and pending field count + percent = f"{((completed_field_count / 15) * 100):.1f}" + employee_info = { + "employee": employee, + "completed_field_count": percent, + } + employees_with_pending.append(employee_info) + else: + pass + + emps = filtersubordinatesemployeemodel( + request, + Employee.objects.filter(employee_work_info__isnull=True), + perm="employee.view_employeeworkinformation", + ) + for emp in emps: + employees_with_pending.insert( + 0, + { + "employee": Workinfo(employee=emp), + "completed_field_count": "0", + }, + ) + + employees_with_pending.sort(key=lambda x: float(x["completed_field_count"])) + + employees_with_pending = paginator_qry( + employees_with_pending, request.GET.get("page") + ) + + return render( + request, + "work_info_complete.html", + {"employees_with_pending": employees_with_pending}, + ) + + +@login_required +def common_settings(request): + """ + This method is used to render setting page template + """ + return render(request, "settings.html") + + +@login_required +@hx_request_required +@permission_required("auth.add_group") +def user_group_table(request): + """ + Group assign htmx view + """ + permissions = [] + apps = APPS + no_permission_models = NO_PERMISSION_MODALS + form = UserGroupForm() + for app_name in apps: + app_models = [] + for model in get_models_in_app(app_name): + app_models.append( + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + ) + permissions.append({"app": app_name.capitalize(), "app_models": app_models}) + if request.method == "POST": + form = UserGroupForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("User group created.")) + return HttpResponse("") + return render( + request, + "base/auth/group_assign.html", + { + "permissions": permissions, + "form": form, + "no_permission_models": no_permission_models, + }, + ) + + +@login_required +@require_http_methods(["POST"]) +@permission_required("auth.add_permission") +def update_group_permission( + request, +): + """ + This method is used to remove user permission. + """ + group_id = request.POST["id"] + instance = Group.objects.get(id=group_id) + form = UserGroupForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + return JsonResponse({"message": "Updated the permissions", "type": "success"}) + if request.POST.get("name_update"): + name = request.POST["name"] + if len(name) > 3: + instance.name = name + instance.save() + return JsonResponse({"message": "Name updated", "type": "success"}) + return JsonResponse( + {"message": "At least 4 characters required", "type": "success"} + ) + perms = form.cleaned_data.get("permissions") + if not perms: + instance.permissions.clear() + return JsonResponse({"message": "All permission cleared", "type": "info"}) + return JsonResponse({"message": "Something went wrong", "type": "danger"}) + + +@login_required +@permission_required("auth.view_group") +def user_group(request): + """ + This method is used to create user permission group + """ + permissions = [] + + apps = APPS + no_permission_models = NO_PERMISSION_MODALS + form = UserGroupForm() + for app_name in apps: + app_models = [] + for model in get_models_in_app(app_name): + app_models.append( + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + ) + permissions.append( + {"app": app_name.capitalize().replace("_", " "), "app_models": app_models} + ) + groups = Group.objects.all() + return render( + request, + "base/auth/group.html", + { + "permissions": permissions, + "form": form, + "groups": paginator_qry(groups, request.GET.get("page")), + "no_permission_models": no_permission_models, + }, + ) + + +@login_required +@permission_required("auth.view_group") +def user_group_search(request): + """ + This method is used to create user permission group + """ + permissions = [] + + apps = APPS + no_permission_models = NO_PERMISSION_MODALS + form = UserGroupForm() + for app_name in apps: + app_models = [] + for model in get_models_in_app(app_name): + app_models.append( + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + ) + permissions.append({"app": app_name.capitalize(), "app_models": app_models}) + search = "" + if request.GET.get("search"): + search = str(request.GET["search"]) + groups = Group.objects.filter(name__icontains=search) + return render( + request, + "base/auth/group_lines.html", + { + "permissions": permissions, + "form": form, + "groups": paginator_qry(groups, request.GET.get("page")), + "no_permission_models": no_permission_models, + }, + ) + + +@login_required +@hx_request_required +@permission_required("auth.add_group") +def group_assign(request): + """ + This method is used to assign user group to the users. + """ + group_id = request.GET.get("group") + form = AssignUserGroup( + initial={ + "group": group_id, + "employee": Employee.objects.filter( + employee_user_id__groups__id=group_id + ).values_list("id", flat=True), + } + ) + if request.POST: + group_id = request.POST["group"] + form = AssignUserGroup( + {"group": group_id, "employee": request.POST.getlist("employee")} + ) + if form.is_valid(): + form.save() + messages.success(request, _("User group assigned.")) + return HttpResponse("") + return render( + request, + "base/auth/group_user_assign.html", + {"form": form, "group_id": group_id}, + ) + + +@login_required +@permission_required("auth.view_group") +def group_assign_view(request): + """ + This method is used to search the user groups + """ + search = "" + if request.GET.get("search") is not None: + search = request.GET.get("search") + groups = Group.objects.filter(name__icontains=search) + previous_data = request.GET.urlencode() + return render( + request, + "base/auth/group_assign_view.html", + {"groups": paginator_qry(groups, request.GET.get("page")), "pd": previous_data}, + ) + + +@login_required +@permission_required("auth.view_group") +def user_group_view(request): + """ + This method is used to render template for view all groups + """ + search = "" + if request.GET.get("search") is not None: + search = request.GET["search"] + user_group = Group.objects.filter() + return render(request, "base/auth/group_assign.html", {"data": user_group}) + + +@login_required +@permission_required("change_group") +@require_http_methods(["POST"]) +def user_group_permission_remove(request, pid, gid): + """ + This method is used to remove permission from group. + args: + pid: permission id + gid: group id + """ + group = Group.objects.get(id=1) + permission = Permission.objects.get(id=2) + group.permissions.remove(permission) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@permission_required("change_group") +@require_http_methods(["POST"]) +def group_remove_user(request, uid, gid): + """ + This method is used to remove an user from group permission. + args: + uid: user instance id + gid: group instance id + """ + group = Group.objects.get(id=gid) + user = User.objects.get(id=uid) + group.user_set.remove(user) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@delete_permission() +@require_http_methods(["POST", "DELETE"]) +def object_delete(request, obj_id, **kwargs): + """ + Handles the deletion of an object instance from the database. + + Args: + request (HttpRequest): The HTTP request object containing metadata about + the request and user. + obj_id (int): The ID of the object to be deleted. + **kwargs: Additional keyword arguments including: + - model (Model): The Django model class to which the object belongs. + - redirect_path (str): The URL path to redirect to after deletion. + Returns: + HttpResponse: Redirects to the specified `redirect_path` or reloads the + previous page. In case of a ProtectedError, it shows an error + message indicating that the object is in use. + """ + model = kwargs.get("model") + redirect_path = kwargs.get("redirect_path") + delete_error = False + try: + instance = model.objects.get(id=obj_id) + instance.delete() + messages.success( + request, _("The {} has been deleted successfully.").format(instance) + ) + except model.DoesNotExist: + delete_error = True + messages.error(request, _("{} not found.").format(model._meta.verbose_name)) + except ProtectedError as e: + model_verbose_names_set = set() + for obj in e.protected_objects: + model_verbose_names_set.add(_(obj._meta.verbose_name.capitalize())) + + model_names_str = ", ".join(model_verbose_names_set) + delete_error = True + messages.error( + request, + _("This {} is already in use for {}.").format(instance, model_names_str), + ), + + if apps.is_installed("pms") and redirect_path == "/pms/filter-key-result/": + KeyResult = get_horilla_model_class(app_label="pms", model="keyresult") + key_results = KeyResult.objects.all() + if key_results.exists(): + previous_data = request.GET.urlencode() + redirect_path = redirect_path + "?" + previous_data + return redirect(redirect_path) + else: + return HttpResponse("") + + if redirect_path: + previous_data = request.GET.urlencode() + redirect_path = redirect_path + "?" + previous_data + return redirect(redirect_path) + elif kwargs.get("HttpResponse"): + if delete_error: + return_part = "" + elif kwargs.get("HttpResponse") is True: + return_part = "" + else: + return_part = kwargs.get("HttpResponse") + return HttpResponse(f"{return_part}") + else: + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@hx_request_required +@duplicate_permission() +def object_duplicate(request, obj_id, **kwargs): + """ + Handles the duplication of an object instance in the database. + + Args: + request (HttpRequest): The HTTP request object containing metadata about + the request and user. + obj_id (int): The ID of the object to be duplicated. + **kwargs: Additional keyword arguments including: + - model (Model): The Django model class to which the object belongs. + - form (Form): The Django form class used to handle the object data. + - template (str): The template to render for the duplication process. + - form_name (str, optional): The name to use for the form in the template context. + + Returns: + HttpResponse: Renders the duplication form on GET requests and, on successful + POST, reloads the page after saving the duplicated object. + """ + model = kwargs["model"] + form_class = kwargs["form"] + template = kwargs["template"] + try: + original_object = model.objects.get(id=obj_id) + except model.DoesNotExist: + messages.error(request, f"{model._meta.verbose_name} object does not exist.") + if request.headers.get("HX-Request"): + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + else: + current_url = request.META.get("HTTP_REFERER", "/") + return HttpResponseRedirect(current_url) + + form = form_class(instance=original_object) + search_words = ( + form.get_template_language() if hasattr(form, "get_template_language") else None + ) + if request.method == "GET": + for field_name, field in form.fields.items(): + if isinstance(field, forms.CharField): + if field.initial: + initial_value = field.initial + else: + initial_value = f"{form.initial.get(field_name, '')} (copy)" + form.initial[field_name] = initial_value + form.fields[field_name].initial = initial_value + if hasattr(form.instance, "id"): + form.instance.id = None + + if request.method == "POST": + form = form_class(request.POST) + if form.is_valid(): + new_object = form.save(commit=False) + new_object.id = None + new_object.save() + return HttpResponse("") + context = { + kwargs.get("form_name", "form"): form, + "obj_id": obj_id, + "duplicate": True, + "searchWords": search_words, + } + return render(request, template, context) + + +@login_required +@hx_request_required +@duplicate_permission() +def add_remove_dynamic_fields(request, **kwargs): + """ + Handles the dynamic addition and removal of form fields in a Django form. + + Args: + request (HttpRequest): The HTTP request object containing metadata about + the request and user. + **kwargs: Additional keyword arguments including: + - model (Model): The Django model class used for `ModelChoiceField`. + - form_class (Form): The Django form class to which dynamic fields will be added. + - template (str): The template used to render the newly added field. + - empty_label (str, optional): The label to show for empty choices in + a `ModelChoiceField`. + - field_name_pre (str): The prefix for the dynamically generated field names. + - field_type (str, optional): The type of field to add, either "character" + or "model_choice". + + Returns: + HttpResponse: Returns the HTML for the newly added field, rendered in the context of the + specified template. If the request is not POST or if no valid HTMX target + is provided, it returns an empty HTTP response. + """ + if request.method == "POST": + model = kwargs["model"] + form_class = kwargs["form_class"] + template = kwargs["template"] + empty_label = kwargs.get("empty_label") + field_name_pre = kwargs["field_name_pre"] + field_type = kwargs.get("field_type") + hx_target = request.META.get("HTTP_HX_TARGET") + if hx_target: + field_counts = int(hx_target.split("_")[-1]) + 1 + next_hx_target = f"{hx_target.rsplit('_', 1)[0]}_{field_counts}" + form = form_class() + field_name = f"{field_name_pre}{field_counts}" + if field_type and field_type == "character": + form.fields[field_name] = forms.CharField( + widget=forms.TextInput( + attrs={ + "class": "oh-input w-100", + "name": field_name, + "id": f"id_{field_name}", + } + ), + required=False, + ) + else: + form.fields[field_name] = forms.ModelChoiceField( + queryset=model.objects.all(), + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2 mb-3", + "name": field_name, + "id": f"id_{field_name}", + } + ), + required=False, + empty_label=empty_label, + ) + context = { + "field_counts": field_counts, + "field_html": form[field_name].as_widget(), + "current_hx_target": hx_target, + "next_hx_target": next_hx_target, + } + field_html = render_to_string(template, context) + return HttpResponse(field_html) + return HttpResponse() + + +@login_required +@permission_required("base.view_dynamicemailconfiguration") +def mail_server_conf(request): + mail_servers = DynamicEmailConfiguration.objects.all() + primary_mail_not_exist = True + if DynamicEmailConfiguration.objects.filter(is_primary=True).exists(): + primary_mail_not_exist = False + return render( + request, + "base/mail_server/mail_server.html", + { + "mail_servers": mail_servers, + "primary_mail_not_exist": primary_mail_not_exist, + }, + ) + + +@login_required +@permission_required("base.view_dynamicemailconfiguration") +def mail_server_test_email(request): + instance_id = request.GET.get("instance_id") + white_labelling = getattr(horilla_apps, "WHITE_LABELLING", False) + image_path = path.join(settings.STATIC_ROOT, "images/ui/horilla-logo.png") + company_name = "Horilla" + + if white_labelling: + hq = Company.objects.filter(hq=True).last() + try: + company = ( + request.user.employee_get.get_company() + if request.user.employee_get.get_company() + else hq + ) + except: + company = hq + + if company: + company_name = company.company + image_path = path.join(settings.MEDIA_ROOT, company.icon.name) + + form = DynamicMailTestForm() + if request.method == "POST": + form = DynamicMailTestForm(request.POST) + if form.is_valid(): + email_to = form.cleaned_data["to_email"] + subject = _("Test mail from Horilla") + + # HTML content + html_content = f""" + + + + + + + + + + + + +
+

{company_name}

+
+

Email tested successfully

+

Hi,
+ This email is being sent as part of mail sever testing from {company_name}.

+ Test Image +
+

© {datetime.today().year} {company_name}

+
+ + + """ + + # Plain text content (fallback for email clients that do not support HTML) + text_content = strip_tags(html_content) + + email_backend = ConfiguredEmailBackend() + emailconfig = DynamicEmailConfiguration.objects.filter( + id=instance_id + ).first() + email_backend.configuration = emailconfig + + try: + msg = EmailMultiAlternatives( + subject, + text_content, + email_backend.dynamic_from_email_with_display_name, + [email_to], + connection=email_backend, + ) + msg.attach_alternative(html_content, "text/html") + + with open(image_path, "rb") as img: + msg_img = MIMEImage(img.read()) + msg_img.add_header("Content-ID", "") + msg.attach(msg_img) + + msg.send() + + except Exception as e: + messages.error(request, " ".join([_("Something went wrong :"), str(e)])) + return HttpResponse("") + + messages.success(request, _("Mail sent successfully")) + return HttpResponse("") + return render( + request, + "base/mail_server/form_email_test.html", + {"form": form, "instance_id": instance_id}, + ) + + +@login_required +@permission_required("base.delete_dynamicemailconfiguration") +def mail_server_delete(request): + """ + This method is used to delete mail server + """ + ids = request.GET.getlist("ids") + # primary_mail_check + delete = True + for id in ids: + emailconfig = DynamicEmailConfiguration.objects.filter(id=id).first() + if emailconfig.is_primary: + delete = False + if delete: + DynamicEmailConfiguration.objects.filter(id__in=ids).delete() + messages.success(request, "Mail server configuration deleted") + return HttpResponse("") + else: + if DynamicEmailConfiguration.objects.all().count() == 1: + messages.warning( + request, + "You have only 1 Mail server configuration that can't be deleted", + ) + return HttpResponse("") + else: + mails = DynamicEmailConfiguration.objects.all().exclude(is_primary=True) + return render( + request, + "base/mail_server/replace_mail.html", + { + "mails": mails, + "title": _("Can't Delete"), + }, + ) + + +def replace_primary_mail(request): + """ + This method is used to replace primary mail server + """ + emailconfig_id = request.POST.get("replace_mail") + email_config = DynamicEmailConfiguration.objects.get(id=emailconfig_id) + email_config.is_primary = True + email_config.save() + DynamicEmailConfiguration.objects.filter(is_primary=True).first().delete() + + messages.success(request, "Primary Mail server configuration replaced") + return redirect("mail-server-conf") + + +@login_required +@hx_request_required +@permission_required("base.add_dynamicemailconfiguration") +def mail_server_create_or_update(request): + instance_id = request.GET.get("instance_id") + instance = None + if instance_id: + instance = DynamicEmailConfiguration.objects.filter(id=instance_id).first() + form = DynamicMailConfForm(instance=instance) + if request.method == "POST": + form = DynamicMailConfForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + return HttpResponse("") + return render( + request, "base/mail_server/form.html", {"form": form, "instance": instance} + ) + + +@login_required +@permission_required("base.view_horillamailtemplate") +def view_mail_templates(request): + """ + This method will render template to disply the offerletter templates + """ + templates = HorillaMailTemplate.objects.all() + form = MailTemplateForm() + if templates.exists(): + template = "mail/view_templates.html" + else: + template = "mail/empty_mail_template.html" + searchWords = form.get_template_language() + return render( + request, + template, + {"templates": templates, "form": form, "searchWords": searchWords}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_horillamailtemplate") +def view_mail_template(request, obj_id): + """ + This method is used to display the template/form to edit + """ + template = HorillaMailTemplate.objects.get(id=obj_id) + form = MailTemplateForm(instance=template) + searchWords = form.get_template_language() + if request.method == "POST": + form = MailTemplateForm(request.POST, instance=template) + if form.is_valid(): + form.save() + messages.success(request, "Template updated") + return HttpResponse("") + + return render( + request, + "mail/htmx/form.html", + {"form": form, "duplicate": False, "searchWords": searchWords}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_horillamailtemplate") +def create_mail_templates(request): + """ + This method is used to create offerletter template + """ + form = MailTemplateForm() + searchWords = form.get_template_language() + + if request.method == "POST": + form = MailTemplateForm(request.POST) + if form.is_valid(): + instance = form.save() + instance.save() + messages.success(request, "Template created") + return HttpResponse("") + + return render( + request, + "mail/htmx/form.html", + {"form": form, "duplicate": False, "searchWords": searchWords}, + ) + + +@login_required +@permission_required("base.delete_horillamailtemplate") +def delete_mail_templates(request): + ids = request.GET.getlist("ids") + result = HorillaMailTemplate.objects.filter(id__in=ids).delete() + messages.success(request, "Template deleted") + return redirect(view_mail_templates) + + +@login_required +@hx_request_required +@permission_required("base.add_company") +def company_create(request): + """ + This method render template and form to create company and save if the form is valid + """ + + form = CompanyForm() + companies = Company.objects.all() + if request.method == "POST": + form = CompanyForm(request.POST, request.FILES) + + if form.is_valid(): + form.save() + + messages.success(request, _("Company has been created successfully!")) + return HttpResponse("") + + return render( + request, + "base/company/company_form.html", + {"form": form, "companies": companies}, + ) + + +@login_required +@permission_required("base.view_company") +def company_view(request): + """ + This method used to view created companies + """ + companies = Company.objects.all() + return render( + request, + "base/company/company.html", + {"companies": companies, "model": Company()}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_company") +def company_update(request, id, **kwargs): + """ + This method is used to update company + args: + id : company instance id + + """ + company = Company.objects.get(id=id) + form = CompanyForm(instance=company) + if request.method == "POST": + form = CompanyForm(request.POST, request.FILES, instance=company) + if form.is_valid(): + form.save() + messages.success(request, _("Company updated")) + return HttpResponse("") + return render( + request, "base/company/company_form.html", {"form": form, "company": company} + ) + + +@login_required +@hx_request_required +@permission_required("base.add_department") +def department_create(request): + """ + This method renders form and template to create department + """ + + form = DepartmentForm() + if request.method == "POST": + form = DepartmentForm(request.POST) + if form.is_valid(): + form.save() + form = DepartmentForm() + messages.success(request, _("Department has been created successfully!")) + return HttpResponse("") + return render( + request, + "base/department/department_form.html", + { + "form": form, + }, + ) + + +@login_required +@permission_required("base.view_department") +def department_view(request): + """ + This method view department + """ + departments = Department.objects.all() + return render( + request, + "base/department/department.html", + { + "departments": departments, + }, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_department") +def department_update(request, id, **kwargs): + """ + This method is used to update department + args: + id : department instance id + """ + department = Department.find(id) + form = DepartmentForm(instance=department) + if request.method == "POST": + form = DepartmentForm(request.POST, instance=department) + if form.is_valid(): + form.save() + messages.success(request, _("Department updated.")) + return HttpResponse("") + return render( + request, + "base/department/department_form.html", + {"form": form, "department": department}, + ) + + +@login_required +@permission_required("base.view_jobposition") +def job_position(request): + """ + This method is used to view job position + """ + + departments = Department.objects.all() + jobs = False + if JobPosition.objects.exists(): + jobs = True + form = JobPositionForm() + if request.method == "POST": + form = JobPositionForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Job Position has been created successfully!")) + return render( + request, + "base/job_position/job_position.html", + {"form": form, "departments": departments, "jobs": jobs}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_jobposition") +def job_position_creation(request): + """ + This method is used to create job position + """ + dynamic = request.GET.get("dynamic") if request.GET.get("dynamic") else "" + form = JobPositionMultiForm() + if request.method == "POST": + form = JobPositionMultiForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Job Position has been created successfully!")) + return HttpResponse("") + return render( + request, + "base/job_position/job_position_form.html", + { + "form": form, + "dynamic": dynamic, + }, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_jobposition") +def job_position_update(request, id, **kwargs): + """ + This method is used to update job position + args: + id : job position instance id + + """ + job_position = JobPosition.find(id) + form = JobPositionForm(instance=job_position) + if request.method == "POST": + form = JobPositionForm(request.POST, instance=job_position) + if form.is_valid(): + form.save(commit=True) + messages.success(request, _("Job position updated.")) + return HttpResponse("") + return render( + request, + "base/job_position/job_position_form.html", + {"form": form, "job_position": job_position}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_jobrole") +def job_role_create(request): + """ + This method is used to create job role. + """ + dynamic = request.GET.get("dynamic") + form = JobRoleForm() + if request.method == "POST": + form = JobRoleForm(request.POST) + if form.instance.pk and form.is_valid(): + form.save(commit=True) + messages.success(request, _("Job role has been created successfully!")) + elif ( + not form.instance.pk + and form.data.getlist("job_position_id") + and form.data.get("job_role") + ): + form.save(commit=True) + messages.success(request, _("Job role has been created successfully!")) + return HttpResponse("") + + return render( + request, + "base/job_role/job_role_form.html", + { + "form": form, + "dynamic": dynamic, + }, + ) + + +@login_required +@permission_required("base.view_jobrole") +def job_role_view(request): + """ + This method is used to view job role. + """ + + jobs = JobPosition.objects.all() + job_role = False + if JobRole.objects.exists(): + job_role = True + + return render( + request, + "base/job_role/job_role.html", + {"job_positions": jobs, "job_role": job_role}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_jobrole") +def job_role_update(request, id, **kwargs): + """ + This method is used to update job role instance + args: + id : job role instance id + + """ + + job_role = JobRole.find(id) + form = JobRoleForm(instance=job_role) + if request.method == "POST": + form = JobRoleForm(request.POST, instance=job_role) + if form.is_valid(): + form.save(commit=True) + messages.success(request, _("Job role updated.")) + return HttpResponse("") + + return render( + request, + "base/job_role/job_role_form.html", + { + "form": form, + "job_role": job_role, + }, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_worktype") +def work_type_create(request): + """ + This method is used to create work type + """ + dynamic = request.GET.get("dynamic") + form = WorkTypeForm() + work_types = WorkType.objects.all() + if request.method == "POST": + form = WorkTypeForm(request.POST) + if form.is_valid(): + form.save() + form = WorkTypeForm() + + messages.success(request, _("Work Type has been created successfully!")) + return HttpResponse("") + + return render( + request, + "base/work_type/work_type_form.html", + {"form": form, "work_types": work_types, "dynamic": dynamic}, + ) + + +@login_required +@permission_required("base.view_worktype") +def work_type_view(request): + """ + This method is used to view work type + """ + + work_types = WorkType.objects.all() + return render( + request, + "base/work_type/work_type.html", + {"work_types": work_types}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_worktype") +def work_type_update(request, id, **kwargs): + """ + This method is used to update work type instance + args: + id : work type instance id + + """ + + work_type = WorkType.find(id) + form = WorkTypeForm(instance=work_type) + if request.method == "POST": + form = WorkTypeForm(request.POST, instance=work_type) + if form.is_valid(): + form.save() + messages.success(request, _("Work type updated.")) + return HttpResponse("") + return render( + request, + "base/work_type/work_type_form.html", + {"form": form, "work_type": work_type}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_rotatingworktype") +def rotating_work_type_create(request): + """ + This method is used to create rotating work type . + """ + + form = RotatingWorkTypeForm() + if request.method == "POST": + form = RotatingWorkTypeForm(request.POST) + if form.is_valid(): + form.save() + form = RotatingWorkTypeForm() + messages.success(request, _("Rotating work type created.")) + return HttpResponse("") + return render( + request, + "base/rotating_work_type/htmx/rotating_work_type_form.html", + {"form": form, "rwork_type": RotatingWorkType.objects.all()}, + ) + + +@login_required +@permission_required("base.view_rotatingworktype") +def rotating_work_type_view(request): + """ + This method is used to view rotating work type . + """ + + return render( + request, + "base/rotating_work_type/rotating_work_type.html", + {"rwork_type": RotatingWorkType.objects.all()}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_rotatingworktype") +def rotating_work_type_update(request, id, **kwargs): + """ + This method is used to update rotating work type instance. + args: + id : rotating work type instance id + + """ + + rotating_work_type = RotatingWorkType.find(id) + form = RotatingWorkTypeForm(instance=rotating_work_type) + if request.method == "POST": + form = RotatingWorkTypeForm(request.POST, instance=rotating_work_type) + if form.is_valid(): + form.save() + messages.success(request, _("Rotating work type updated.")) + return HttpResponse("") + + return render( + request, + "base/rotating_work_type/htmx/rotating_work_type_form.html", + {"form": form, "r_type": rotating_work_type}, + ) + + +@login_required +@manager_can_enter("base.view_rotatingworktypeassign") +def rotating_work_type_assign(request): + """ + This method is used to assign rotating work type to employee users + """ + + filter = RotatingWorkTypeAssignFilter( + queryset=RotatingWorkTypeAssign.objects.filter(is_active=True) + ) + rwork_all = RotatingWorkTypeAssign.objects.all() + rwork_type_assign = filter.qs.order_by("-id") + rwork_type_assign = filtersubordinates( + request, rwork_type_assign, "base.view_rotatingworktypeassign" + ) + rwork_type_assign = rwork_type_assign.filter(employee_id__is_active=True) + assign_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + rwork_type_assign, request.GET.get("page") + ).object_list + ] + ) + return render( + request, + "base/rotating_work_type/rotating_work_type_assign.html", + { + "f": filter, + "rwork_type_assign": paginator_qry( + rwork_type_assign, request.GET.get("page") + ), + "assign_ids": assign_ids, + "rwork_all": rwork_all, + "gp_fields": RotatingWorkTypeRequestReGroup.fields, + }, + ) + + +@login_required +@hx_request_required +@manager_can_enter("base.add_rotatingworktypeassign") +def rotating_work_type_assign_add(request): + """ + This method is used to assign rotating work type + """ + form = RotatingWorkTypeAssignForm() + if request.GET.get("emp_id"): + employee = request.GET.get("emp_id") + form = RotatingWorkTypeAssignUpdateForm(initial={"employee_id": employee}) + form = choosesubordinates(request, form, "base.add_rotatingworktypeassign") + if request.method == "POST": + form = RotatingWorkTypeAssignForm(request.POST) + form = choosesubordinates(request, form, "base.add_rotatingworktypeassign") + if form.is_valid(): + form.save() + employee_ids = request.POST.getlist("employee_id") + employees = Employee.objects.filter(id__in=employee_ids).select_related( + "employee_user_id" + ) + users = [employee.employee_user_id for employee in employees] + notify.send( + request.user.employee_get, + recipient=users, + verb="You are added to rotating work type", + verb_ar="تمت إضافتك إلى نوع العمل المتناوب", + verb_de="Sie werden zum rotierenden Arbeitstyp hinzugefügt", + verb_es="Se le agrega al tipo de trabajo rotativo", + verb_fr="Vous êtes ajouté au type de travail rotatif", + icon="infinite", + redirect=reverse("employee-profile"), + ) + + messages.success(request, _("Rotating work type assigned.")) + response = render( + request, + "base/rotating_work_type/htmx/rotating_work_type_assign_form.html", + {"form": form}, + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "base/rotating_work_type/htmx/rotating_work_type_assign_form.html", + {"form": form}, + ) + + +@login_required +@hx_request_required +@manager_can_enter("base.view_rotatingworktypeassign") +def rotating_work_type_assign_view(request): + """ + This method renders template to view rotating work type objects + """ + + previous_data = request.GET.urlencode() + rwork_type_assign = RotatingWorkTypeAssignFilter(request.GET).qs.order_by("-id") + field = request.GET.get("field") + if not request.GET.get("is_active") or request.GET.get("is_active") in [ + "true", + "unknown", + ]: + rwork_type_assign = rwork_type_assign.filter(is_active=True) + if request.GET.get("is_active") == "false": + rwork_type_assign = rwork_type_assign.filter(is_active=False) + rwork_type_assign = filtersubordinates( + request, rwork_type_assign, "base.view_rotatingworktypeassign" + ) + if request.GET.get("orderby"): + rwork_type_assign = sortby(request, rwork_type_assign, "orderby") + template = "base/rotating_work_type/rotating_work_type_assign_view.html" + data_dict = parse_qs(previous_data) + get_key_instances(RotatingWorkTypeAssign, data_dict) + + if field != "" and field is not None: + rwork_type_assign = group_by_queryset( + rwork_type_assign, field, request.GET.get("page"), "page" + ) + list_values = [entry["list"] for entry in rwork_type_assign] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + assign_ids = json.dumps(list(id_list)) + template = "base/rotating_work_type/htmx/group_by.html" + + else: + rwork_type_assign = paginator_qry(rwork_type_assign, request.GET.get("page")) + assign_ids = json.dumps( + [instance.id for instance in rwork_type_assign.object_list] + ) + + return render( + request, + template, + { + "rwork_type_assign": rwork_type_assign, + "pd": previous_data, + "filter_dict": data_dict, + "assign_ids": assign_ids, + "field": field, + }, + ) + + +@login_required +@hx_request_required +def rotating_work_individual_view(request, instance_id): + """ + This view is used render detailed view of the rotating work type assign + """ + request_copy = request.GET.copy() + request_copy.pop("instances_ids", None) + previous_data = request_copy.urlencode() + instance = RotatingWorkTypeAssign.objects.filter(id=instance_id).first() + context = {"instance": instance, "pd": previous_data} + assign_ids_json = request.GET.get("instances_ids") + if assign_ids_json: + assign_ids = json.loads(assign_ids_json) + previous_id, next_id = closest_numbers(assign_ids, instance_id) + context["previous"] = previous_id + context["next"] = next_id + context["assign_ids"] = assign_ids_json + HTTP_REFERER = request.META.get("HTTP_REFERER", None) + context["close_hx_url"] = "" + context["close_hx_target"] = "" + if HTTP_REFERER and HTTP_REFERER.endswith("rotating-work-type-assign/"): + context["close_hx_url"] = "/rotating-work-type-assign-view" + context["close_hx_target"] = "#view-container" + elif HTTP_REFERER: + HTTP_REFERERS = [part for part in HTTP_REFERER.split("/") if part] + try: + employee_id = int(HTTP_REFERERS[-1]) + context["close_hx_url"] = f"/employee/shift-tab/{employee_id}" + context["close_hx_target"] = "#shift_target" + except ValueError: + pass + return render(request, "base/rotating_work_type/individual_view.html", context) + + +@login_required +@hx_request_required +@manager_can_enter("base.change_rotatingworktypeassign") +def rotating_work_type_assign_update(request, id): + """ + This method is used to update rotating work type instance + """ + + rotating_work_type_assign_obj = RotatingWorkTypeAssign.objects.get(id=id) + form = RotatingWorkTypeAssignUpdateForm(instance=rotating_work_type_assign_obj) + form = choosesubordinates(request, form, "base.change_rotatingworktypeassign") + if request.method == "POST": + form = RotatingWorkTypeAssignUpdateForm( + request.POST, instance=rotating_work_type_assign_obj + ) + form = choosesubordinates(request, form, "base.change_rotatingworktypeassign") + if form.is_valid(): + form.save() + messages.success(request, _("Rotating work type assign updated.")) + response = render( + request, + "base/rotating_work_type/htmx/rotating_work_type_assign_update_form.html", + {"update_form": form}, + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "base/rotating_work_type/htmx/rotating_work_type_assign_update_form.html", + {"update_form": form}, + ) + + +@login_required +@manager_can_enter("base.change_rotatingworktypeassign") +def rotating_work_type_assign_export(request): + if request.META.get("HTTP_HX_REQUEST") == "true": + context = { + "export_filter": RotatingWorkTypeAssignFilter(), + "export_columns": RotatingWorkTypeAssignExportForm(), + } + return render( + request, + "base/rotating_work_type/rotating_work_type_assign_export.html", + context=context, + ) + + return export_data( + request=request, + model=RotatingWorkTypeAssign, + filter_class=RotatingWorkTypeAssignFilter, + form_class=RotatingWorkTypeAssignExportForm, + file_name="Rotating_work_type_assign", + ) + + +def rotating_work_type_assign_redirect(request, obj_id=None, employee_id=None): + request_copy = request.GET.copy() + request_copy.pop("instances_ids", None) + previous_data = request_copy.urlencode() + hx_target = request.META.get("HTTP_HX_TARGET", None) + if hx_target and hx_target == "view-container": + return redirect(f"/rotating-work-type-assign-view?{previous_data}") + elif hx_target and hx_target == "objectDetailsModalTarget": + instances_ids = request.GET.get("instances_ids") + instances_list = json.loads(instances_ids) + if obj_id in instances_list: + instances_list.remove(obj_id) + previous_instance, next_instance = closest_numbers( + json.loads(instances_ids), obj_id + ) + + url = f"/rwork-individual-view/{next_instance}/" + params = f"?{previous_data}&instances_ids={instances_list}" + return redirect(url + params) + elif hx_target and hx_target == "shift_target" and employee_id: + return redirect(f"/employee/shift-tab/{employee_id}") + elif hx_target: + return HttpResponse("") + else: + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@hx_request_required +@manager_can_enter("base.change_rotatingworktypeassign") +def rotating_work_type_assign_archive(request, obj_id): + """ + Archive or un-archive rotating work type assigns + """ + try: + rwork_type = get_object_or_404(RotatingWorkTypeAssign, id=obj_id) + employee_id = rwork_type.employee_id.id + employees_rwork_types = RotatingWorkTypeAssign.objects.filter( + is_active=True, employee_id=rwork_type.employee_id + ) + rwork_type.is_active = not rwork_type.is_active + if rwork_type.is_active and employees_rwork_types: + messages.error(request, "Already on record is active") + else: + rwork_type.save() + message = _("un-archived") if rwork_type.is_active else _("archived") + messages.success( + request, _("Rotating work type assign is {}").format(message) + ) + return rotating_work_type_assign_redirect(request, obj_id, employee_id) + except Http404: + messages.error(request, _("Rotating work type assign not found.")) + return rotating_work_type_assign_redirect(request, obj_id, employee_id) + + +@login_required +@manager_can_enter("base.change_rotatingworktypeassign") +def rotating_work_type_assign_bulk_archive(request): + """ + This method is used to archive/un-archive bulk rotating work type assigns. + """ + ids = json.loads(request.POST["ids"]) + is_active = request.POST.get("is_active") != "false" + message = _("un-archived") if is_active else _("archived") + count = 0 + + for id in ids: + rwork_type_assign = RotatingWorkTypeAssign.objects.get(id=id) + employees_rwork_type_assign = RotatingWorkTypeAssign.objects.filter( + is_active=True, employee_id=rwork_type_assign.employee_id + ) + + if is_active and employees_rwork_type_assign.exists(): + messages.error( + request, + _("Rotating work type for {employee_id} already exists").format( + employee_id=rwork_type_assign.employee_id, + ), + ) + else: + rwork_type_assign.is_active = is_active + rwork_type_assign.save() + count += 1 + + if count > 0: + messages.success( + request, + _("Rotating work type for {count} employees is {message}").format( + count=count, message=message + ), + ) + + return rotating_work_type_assign_redirect(request) + + +@login_required +@permission_required("base.delete_rotatingworktypeassign") +def rotating_work_type_assign_bulk_delete(request): + """ + This method is used to archive/un-archive bulk rotating work type assigns + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for id in ids: + try: + rwork_type_assign = RotatingWorkTypeAssign.objects.get(id=id) + rwork_type_assign.delete() + messages.success( + request, + _("{employee} deleted.").format(employee=rwork_type_assign.employee_id), + ) + except RotatingWorkTypeAssign.DoesNotExist: + messages.error(request, _("{rwork_type_assign} not found.")) + except ProtectedError: + messages.error( + request, + _("You cannot delete {rwork_type_assign}").format( + rwork_type_assign=rwork_type_assign + ), + ) + return JsonResponse({"message": "Success"}) + + +@login_required +@hx_request_required +@permission_required("base.delete_rotatingworktypeassign") +@require_http_methods(["POST"]) +def rotating_work_type_assign_delete(request, obj_id): + """ + This method is used to delete rotating work type + """ + try: + rotating_work_type_assign_obj = RotatingWorkTypeAssign.objects.get(id=obj_id) + employee_id = rotating_work_type_assign_obj.employee_id.id + rotating_work_type_assign_obj.delete() + messages.success(request, _("Rotating work type assign deleted.")) + except RotatingWorkTypeAssign.DoesNotExist: + messages.error(request, _("Rotating work type assign not found.")) + except ProtectedError: + messages.error(request, _("You cannot delete this rotating work type.")) + + return rotating_work_type_assign_redirect(request, obj_id, employee_id) + + +@login_required +@permission_required("base.view_employeetype") +def employee_type_view(request): + """ + This method is used to view employee type + """ + + types = EmployeeType.objects.all() + return render( + request, + "base/employee_type/employee_type.html", + { + "employee_types": types, + }, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_employeetype") +def employee_type_create(request): + """ + This method is used to create employee type + """ + dynamic = request.GET.get("dynamic") + form = EmployeeTypeForm() + types = EmployeeType.objects.all() + if request.method == "POST": + form = EmployeeTypeForm(request.POST) + if form.is_valid(): + form.save() + form = EmployeeTypeForm() + messages.success(request, _("Employee type created.")) + return HttpResponse("") + return render( + request, + "base/employee_type/employee_type_form.html", + {"form": form, "employee_types": types, "dynamic": dynamic}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_employeetype") +def employee_type_update(request, id, **kwargs): + """ + This method is used to update employee type instance + args: + id : employee type instance id + + """ + + employee_type = EmployeeType.find(id) + form = EmployeeTypeForm(instance=employee_type) + if request.method == "POST": + form = EmployeeTypeForm(request.POST, instance=employee_type) + if form.is_valid(): + form.save() + messages.success(request, _("Employee type updated.")) + return HttpResponse("") + return render( + request, + "base/employee_type/employee_type_form.html", + {"form": form, "employee_type": employee_type}, + ) + + +@login_required +@permission_required("base.view_employeeshift") +def employee_shift_view(request): + """ + This method is used to view employee shift + """ + + shifts = EmployeeShift.objects.all() + if apps.is_installed("attendance"): + GraceTime = get_horilla_model_class(app_label="attendance", model="gracetime") + grace_times = GraceTime.objects.all().exclude(is_default=True) + else: + grace_times = None + return render( + request, "base/shift/shift.html", {"shifts": shifts, "grace_times": grace_times} + ) + + +@login_required +@hx_request_required +@permission_required("base.add_employeeshift") +def employee_shift_create(request): + """ + This method is used to create employee shift + """ + dynamic = request.GET.get("dynamic") + form = EmployeeShiftForm() + shifts = EmployeeShift.objects.all() + if request.method == "POST": + form = EmployeeShiftForm(request.POST) + if form.is_valid(): + form.save() + form = EmployeeShiftForm() + messages.success( + request, _("Employee Shift has been created successfully!") + ) + return HttpResponse("") + return render( + request, + "base/shift/shift_form.html", + {"form": form, "shifts": shifts, "dynamic": dynamic}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_employeeshift") +def employee_shift_update(request, id, **kwargs): + """ + This method is used to update employee shift instance + args: + id : employee shift id + + """ + employee_shift = EmployeeShift.find(id) + form = EmployeeShiftForm(instance=employee_shift) + if request.method == "POST": + form = EmployeeShiftForm(request.POST, instance=employee_shift) + if form.is_valid(): + form.save() + messages.success(request, _("Shift updated")) + return HttpResponse("") + return render( + request, "base/shift/shift_form.html", {"form": form, "shift": employee_shift} + ) + + +@login_required +@permission_required("base.view_employeeshiftschedule") +def employee_shift_schedule_view(request): + """ + This method is used to view schedule for shift + """ + shift_schedule = False + if EmployeeShiftSchedule.objects.exists(): + shift_schedule = True + shifts = EmployeeShift.objects.all() + + return render( + request, + "base/shift/schedule.html", + {"shifts": shifts, "shift_schedule": shift_schedule}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_employeeshiftschedule") +def employee_shift_schedule_create(request): + """ + This method is used to create schedule for shift + """ + + form = EmployeeShiftScheduleForm() + shifts = EmployeeShift.objects.all() + if request.method == "POST": + form = EmployeeShiftScheduleForm(request.POST) + if form.is_valid(): + form.save() + form = EmployeeShiftScheduleForm() + messages.success( + request, _("Employee Shift Schedule has been created successfully!") + ) + return HttpResponse("") + + return render( + request, "base/shift/schedule_form.html", {"form": form, "shifts": shifts} + ) + + +@login_required +@hx_request_required +@permission_required("base.change_employeeshiftschedule") +def employee_shift_schedule_update(request, id, **kwargs): + """ + This method is used to update employee shift instance + args: + id : employee shift instance id + """ + + employee_shift_schedule = EmployeeShiftSchedule.find(id) + form = EmployeeShiftScheduleUpdateForm(instance=employee_shift_schedule) + if request.method == "POST": + form = EmployeeShiftScheduleUpdateForm( + request.POST, instance=employee_shift_schedule + ) + if form.is_valid(): + form.save() + messages.success(request, _("Shift schedule created.")) + return HttpResponse("") + return render( + request, + "base/shift/schedule_form.html", + {"form": form, "shift_schedule": employee_shift_schedule}, + ) + + +@login_required +@permission_required("base.view_rotatingshift") +def rotating_shift_view(request): + """ + This method is used to view rotating shift + """ + + return render( + request, + "base/rotating_shift/rotating_shift.html", + {"rshifts": RotatingShift.objects.all()}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_rotatingshift") +def rotating_shift_create(request): + """ + This method is used to create rotating shift + """ + + form = RotatingShiftForm() + if request.method == "POST": + form = RotatingShiftForm(request.POST) + if form.is_valid(): + form.save() + form = RotatingShiftForm() + messages.success(request, _("Rotating shift created.")) + return HttpResponse("") + else: + form = RotatingShiftForm() + return render( + request, + "base/rotating_shift/htmx/rotating_shift_form.html", + {"form": form, "rshifts": RotatingShift.objects.all()}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_rotatingshift") +def rotating_shift_update(request, id, **kwargs): + """ + This method is used to update rotating shift instance + args: + id : rotating shift instance id + """ + + rotating_shift = RotatingShift.find(id) + form = RotatingShiftForm(instance=rotating_shift) + if request.method == "POST": + form = RotatingShiftForm(request.POST, instance=rotating_shift) + if form.is_valid(): + form.save() + form = RotatingShiftForm() + messages.success(request, _("Rotating shift updated.")) + return HttpResponse("") + return render( + request, + "base/rotating_shift/htmx/rotating_shift_form.html", + { + "form": form, + "rshift": rotating_shift, + }, + ) + + +@login_required +@manager_can_enter("base.view_rotatingshiftassign") +def rotating_shift_assign(request): + """ + This method is used to assign rotating shift + """ + form = RotatingShiftAssignForm() + form = choosesubordinates(request, form, "base.add_rotatingshiftassign") + filter = RotatingShiftAssignFilters( + queryset=RotatingShiftAssign.objects.filter(is_active=True) + ) + rshift_assign = filter.qs + rshift_all = RotatingShiftAssign.objects.all() + + rshift_assign = filtersubordinates( + request, rshift_assign, "base.view_rotatingshiftassign" + ) + rshift_assign = rshift_assign.filter(employee_id__is_active=True) + assign_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + rshift_assign, request.GET.get("page") + ).object_list + ] + ) + + return render( + request, + "base/rotating_shift/rotating_shift_assign.html", + { + "form": form, + "f": filter, + "export_filter": RotatingShiftAssignFilters(), + "export_columns": RotatingShiftAssignExportForm(), + "rshift_assign": paginator_qry(rshift_assign, request.GET.get("page")), + "assign_ids": assign_ids, + "rshift_all": rshift_all, + "gp_fields": RotatingShiftRequestReGroup.fields, + }, + ) + + +@login_required +@hx_request_required +@manager_can_enter("base.add_rotatingshiftassign") +def rotating_shift_assign_add(request): + """ + This method is used to add rotating shift assign + """ + form = RotatingShiftAssignForm() + if request.GET.get("emp_id"): + employee = request.GET.get("emp_id") + form = RotatingShiftAssignUpdateForm(initial={"employee_id": employee}) + form = choosesubordinates(request, form, "base.add_rotatingshiftassign") + if request.method == "POST": + form = RotatingShiftAssignForm(request.POST) + form = choosesubordinates(request, form, "base.add_rotatingshiftassign") + if form.is_valid(): + form.save() + employee_ids = request.POST.getlist("employee_id") + employees = Employee.objects.filter(id__in=employee_ids).select_related( + "employee_user_id" + ) + users = [employee.employee_user_id for employee in employees] + notify.send( + request.user.employee_get, + recipient=users, + verb="You are added to rotating shift", + verb_ar="تمت إضافتك إلى وردية الدورية", + verb_de="Sie werden der rotierenden Arbeitsschicht hinzugefügt", + verb_es="Estás agregado a turno rotativo", + verb_fr="Vous êtes ajouté au quart de travail rotatif", + icon="infinite", + redirect=reverse("employee-profile"), + ) + + messages.success(request, _("Rotating shift assigned.")) + response = render( + request, + "base/rotating_shift/htmx/rotating_shift_assign_form.html", + {"form": form}, + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "base/rotating_shift/htmx/rotating_shift_assign_form.html", + {"form": form}, + ) + + +@login_required +@hx_request_required +@manager_can_enter("base.view_rotatingshiftassign") +def rotating_shift_assign_view(request): + """ + This method renders all instance of rotating shift assign to a template + """ + previous_data = request.GET.urlencode() + rshift_assign = RotatingShiftAssignFilters(request.GET).qs.order_by("-id") + field = request.GET.get("field") + if ( + request.GET.get("is_active") is None + or request.GET.get("is_active") == "unknown" + ): + rshift_assign = rshift_assign.filter(is_active=True) + rshift_assign = filtersubordinates( + request, rshift_assign, "base.view_rotatingshiftassign" + ) + rshift_assign = sortby(request, rshift_assign, "orderby") + data_dict = parse_qs(previous_data) + get_key_instances(RotatingShiftAssign, data_dict) + template = "base/rotating_shift/rotating_shift_assign_view.html" + + if field != "" and field is not None: + rshift_assign = group_by_queryset( + rshift_assign, field, request.GET.get("page"), "page" + ) + list_values = [entry["list"] for entry in rshift_assign] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + assign_ids = json.dumps(list(id_list)) + template = "base/rotating_shift/htmx/group_by.html" + + else: + rshift_assign = paginator_qry(rshift_assign, request.GET.get("page")) + assign_ids = json.dumps([instance.id for instance in rshift_assign.object_list]) + return render( + request, + template, + { + "rshift_assign": rshift_assign, + "pd": previous_data, + "filter_dict": data_dict, + "assign_ids": assign_ids, + "field": field, + }, + ) + + +@login_required +@hx_request_required +def rotating_shift_individual_view(request, instance_id): + """ + This view is used render detailed view of the rotating shit assign + """ + request_copy = request.GET.copy() + request_copy.pop("instances_ids", None) + previous_data = request_copy.urlencode() + instance = RotatingShiftAssign.objects.filter(id=instance_id).first() + context = {"instance": instance, "pd": previous_data} + assign_ids_json = request.GET.get("instances_ids") + HTTP_REFERER = request.META.get("HTTP_REFERER", None) + context["close_hx_url"] = "" + context["close_hx_target"] = "" + if HTTP_REFERER and HTTP_REFERER.endswith("rotating-shift-assign/"): + context["close_hx_url"] = "/rotating-shift-assign-view" + context["close_hx_target"] = "#view-container" + elif HTTP_REFERER: + HTTP_REFERERS = [part for part in HTTP_REFERER.split("/") if part] + try: + employee_id = int(HTTP_REFERERS[-1]) + context["close_hx_url"] = f"/employee/shift-tab/{employee_id}" + context["close_hx_target"] = "#shift_target" + except ValueError: + pass + if assign_ids_json: + assign_ids = json.loads(assign_ids_json) + previous_id, next_id = closest_numbers(assign_ids, instance_id) + context["previous"] = previous_id + context["next"] = next_id + context["assign_ids"] = assign_ids + return render(request, "base/rotating_shift/individual_view.html", context) + + +@login_required +@hx_request_required +@manager_can_enter("base.change_rotatingshiftassign") +def rotating_shift_assign_update(request, id): + """ + This method is used to update rotating shift assign instance + args: + id : rotating shift assign instance id + + """ + rotating_shift_assign_obj = RotatingShiftAssign.find(id) + form = RotatingShiftAssignUpdateForm(instance=rotating_shift_assign_obj) + form = choosesubordinates(request, form, "base.change_rotatingshiftassign") + if request.method == "POST": + form = RotatingShiftAssignUpdateForm( + request.POST, instance=rotating_shift_assign_obj + ) + form = choosesubordinates(request, form, "base.change_rotatingshiftassign") + if form.is_valid(): + form.save() + messages.success(request, _("Rotating shift assign updated.")) + response = render( + request, + "base/rotating_shift/htmx/rotating_shift_assign_update_form.html", + { + "update_form": form, + }, + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "base/rotating_shift/htmx/rotating_shift_assign_update_form.html", + { + "update_form": form, + }, + ) + + +@login_required +@manager_can_enter("base.change_rotatingshiftassign") +def rotating_shift_assign_export(request): + if request.META.get("HTTP_HX_REQUEST"): + context = { + "export_filter": RotatingShiftAssignFilters(), + "export_columns": RotatingShiftAssignExportForm(), + } + return render( + request, + "base/rotating_shift/rotating_shift_assign_export.html", + context=context, + ) + return export_data( + request=request, + model=RotatingShiftAssign, + filter_class=RotatingShiftAssignFilters, + form_class=RotatingShiftAssignExportForm, + file_name="Rotating_shift_assign_export", + ) + + +def normalize_list(lst): + return [None if pd.isna(x) else x for x in lst] + + +@login_required +@manager_can_enter("base.add_rotatingworktypeassign") +def rotating_shift_assign_import(request): + if request.method == "POST": + rotating_shift_obj_list = [] + employee_ids = [] + rotating_shift_assign_list = [] + error_list = [] + new_dicts = {} + rotating_shifts = RotatingShift.objects.all() + shifts = EmployeeShift.objects.all() + file = request.FILES["file"] + file_extension = file.name.split(".")[-1].lower() + error = False + create_rotating_shift = True + + existing_dicts = { + rot_shift.id: [ + shift.employee_shift if shift else None + for shift in rot_shift.total_shifts() + ] + for rot_shift in rotating_shifts + } + data_frame = ( + pd.read_csv(file) if file_extension == "csv" else pd.read_excel(file) + ) + work_info_dicts = data_frame.to_dict("records") + try: + keys_list = list(work_info_dicts[0].keys()) + error_dict = {key: [] for key in keys_list} + except: + messages.error(request, "something went wrong....") + data_frame = pd.DataFrame( + ["Please provide valid data"], + columns=["Title Error"], + ) + + error_count = 1 + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="ImportError.csv"' + + data_frame.to_csv(response, index=False) + response["X-Error-Count"] = error_count + return response + + error_dict["Title Error"] = [] + error_dict["Employee Error"] = [] + error_dict["Date Error"] = [] + if len(keys_list) > 4: + start_date = keys_list[3] + start_date = parser.parse(str(start_date), dayfirst=True).date() + + for total_rows, row in enumerate(work_info_dicts, start=1): + employee_ids.append(row["Badge Id"]) + current_list = list(row.values())[3:] + current_list = normalize_list(current_list) + if start_date < datetime.today().date(): + error_dict["Date Error"] = "Start Date must be greater than today" + + if current_list not in list( + existing_dicts.values() + ) and current_list not in list(new_dicts.values()): + if rotating_shifts.filter(name=row["Title"]).exists(): + row["Title Error"] = "Rotating Shift with this Title already exists" + error = True + error_list.append(row) + continue + + rotating_shift_obj = RotatingShift( + name=row["Title"], + shift1=shifts.filter(employee_shift=current_list[0]).first(), + shift2=shifts.filter(employee_shift=current_list[1]).first(), + ) + if current_list[2:]: + additional_data = [] + for item in current_list[2:]: + try: + additional_data.append( + shifts.filter(employee_shift=item).first().id + ) + except: + additional_data.append(None) + + rotating_shift_obj.additional_data = { + "additional_shifts": additional_data + } + rotating_shift_obj.save() + new_dicts[rotating_shift_obj.id] = current_list + + rotating_shift_obj_list.append(rotating_shift_obj) + else: + flag = True + for rot_shift_id, shift_list in existing_dicts.items(): + if shift_list == current_list: + rotating_shift_obj = RotatingShift.objects.get(id=rot_shift_id) + rotating_shift_obj_list.append(rotating_shift_obj) + flag = False + break + if flag: + for rot_shift_id, shift_list in new_dicts.items(): + if shift_list == current_list: + rotating_shift_obj = RotatingShift.objects.get( + id=rot_shift_id + ) + rotating_shift_obj_list.append(rotating_shift_obj) + break + + employee_list = Employee.objects.filter(badge_id__in=employee_ids) + r_shifts = RotatingShiftAssign.objects.all() + if start_date and employee_ids: + for employee, rshift in zip(employee_list, rotating_shift_obj_list): + if not r_shifts.filter( + employee_id=employee, rotating_shift_id=rshift + ).exists(): + rot_shift_assign = RotatingShiftAssign() + rot_shift_assign.employee_id = employee + rot_shift_assign.rotating_shift_id = rshift + rot_shift_assign.start_date = start_date + rot_shift_assign.based_on = "after" + rot_shift_assign.rotate_after_day = 1 + rot_shift_assign.next_change_date = start_date + rot_shift_assign.next_shift = rshift.shift1 + rot_shift_assign.additional_data["next_shift_index"] = 1 + rotating_shift_assign_list.append(rot_shift_assign) + else: + error_message = f"Rotating Shift with ID {rshift.name} is already assigned to employee {employee}" + for row in work_info_dicts: + if row["Badge Id"] == employee.badge_id: + row["Employee Error"] = error_message + error_list.append(row) + break + + create_rotating_shift = ( + not error_list or request.POST.get("create_rotating_shift") == "true" + ) + + if create_rotating_shift: + if rotating_shift_assign_list: + RotatingShiftAssign.objects.bulk_create(rotating_shift_assign_list) + + flg = set() + unique_error_list = [] + + for row in error_list: + badge_id = row["Badge Id"] + if badge_id not in flg: + unique_error_list.append(row) + flg.add(badge_id) + + if unique_error_list: + for item in unique_error_list: + for key, value in error_dict.items(): + if key in item: + value.append(item[key]) + else: + try: + value.append(None) + except: + pass + + keys_to_remove = [ + key + for key, value in error_dict.items() + if all(v is None for v in value) + ] + + for key in keys_to_remove: + del error_dict[key] + data_frame = pd.DataFrame(error_dict, columns=error_dict.keys()) + error_count = len(unique_error_list) + + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="ImportError.csv"' + + data_frame.to_csv(response, index=False) + response["X-Error-Count"] = error_count + return response + + return JsonResponse( + { + "Success": "Employees Imported Succefully", + "success_count": len(employee_list), + } + ) + return HttpResponse("") + + +def rotating_shift_assign_redirect(request, obj_id, employee_id): + request_copy = request.GET.copy() + request_copy.pop("instances_ids", None) + previous_data = request_copy.urlencode() + hx_target = request.META.get("HTTP_HX_TARGET", None) + if hx_target and hx_target == "view-container": + return redirect(f"/rotating-shift-assign-view?{previous_data}") + elif hx_target and hx_target == "objectDetailsModalTarget": + instances_ids = request.GET.get("instances_ids") + instances_list = json.loads(instances_ids) + if obj_id in instances_list: + instances_list.remove(obj_id) + previous_instance, next_instance = closest_numbers( + json.loads(instances_ids), obj_id + ) + url = f"/rshit-individual-view/{next_instance}/" + params = f"?{previous_data}&instances_ids={instances_list}" + return redirect(url + params) + elif hx_target and hx_target == "shift_target" and employee_id: + return redirect(f"/employee/shift-tab/{employee_id}") + elif hx_target: + return HttpResponse("") + else: + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@hx_request_required +@manager_can_enter("base.change_rotatingshiftassign") +def rotating_shift_assign_archive(request, obj_id): + """ + This method is used to archive and unarchive rotating shift assign records + """ + try: + rshift = get_object_or_404(RotatingShiftAssign, id=obj_id) + employee_id = rshift.employee_id.id + employees_rshift_assigns = RotatingShiftAssign.objects.filter( + is_active=True, employee_id=rshift.employee_id + ) + rshift.is_active = not rshift.is_active + if rshift.is_active and employees_rshift_assigns: + messages.error(request, "Already on record is active") + else: + rshift.save() + message = _("un-archived") if rshift.is_active else _("archived") + messages.success(request, _("Rotating shift assign is {}").format(message)) + except Http404: + messages.error(request, _("Rotating shift assign not found.")) + + return rotating_shift_assign_redirect(request, obj_id, employee_id) + + +@login_required +@manager_can_enter("base.change_rotatingshiftassign") +def rotating_shift_assign_bulk_archive(request): + """ + This method is used to archive/un-archive bulk rotating shift assigns + """ + ids = request.POST["ids"] + ids = json.loads(ids) + is_active = True + message = _("un-archived") + if request.GET.get("is_active") == "False": + is_active = False + message = _("archived") + for id in ids: + # check permission right here... + rshift_assign = RotatingShiftAssign.objects.get(id=id) + employees_rshift_assign = RotatingShiftAssign.objects.filter( + is_active=True, employee_id=rshift_assign.employee_id + ) + flag = True + if is_active: + if len(employees_rshift_assign) < 1: + flag = False + rshift_assign.is_active = is_active + else: + flag = False + rshift_assign.is_active = is_active + rshift_assign.save() + if not flag: + messages.success( + request, + _("Rotating shift for {employee} is {message}").format( + employee=rshift_assign.employee_id, message=message + ), + ) + else: + messages.error( + request, + _("Rotating shift for {employee} is already exists").format( + employee=rshift_assign.employee_id + ), + ) + return JsonResponse({"message": "Success"}) + + +@login_required +@manager_can_enter("base.delete_rotatingshiftassign") +def rotating_shift_assign_bulk_delete(request): + """ + This method is used to bulk delete for rotating shift assign + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for id in ids: + try: + rshift_assign = RotatingShiftAssign.objects.get(id=id) + rshift_assign.delete() + messages.success( + request, + _("{employee} assign deleted.").format( + employee=rshift_assign.employee_id + ), + ) + except RotatingShiftAssign.DoesNotExist: + messages.error(request, _("{rshift_assign} not found.")) + except ProtectedError: + messages.error( + request, + _("You cannot delete {rshift_assign}").format( + rshift_assign=rshift_assign + ), + ) + return JsonResponse({"message": "Success"}) + + +@login_required +@hx_request_required +@manager_can_enter("base.delete_rotatingshiftassign") +@require_http_methods(["POST"]) +def rotating_shift_assign_delete(request, obj_id): + """ + This method is used to delete rotating shift assign instance + args: + id : rotating shift assign instance id + """ + try: + rotating_shift_assign_obj = RotatingShiftAssign.objects.get(id=obj_id) + employee_id = rotating_shift_assign_obj.employee_id.id + rotating_shift_assign_obj.delete() + messages.success(request, _("Rotating shift assign deleted.")) + except RotatingShiftAssign.DoesNotExist: + employee_id = None + messages.error(request, _("Rotating shift assign not found.")) + except ProtectedError: + messages.error(request, _("You cannot delete this rotating shift assign.")) + return rotating_shift_assign_redirect(request, obj_id, employee_id) + + +def get_models_in_app(app_name): + """ + get app models + """ + try: + app_config = apps.get_app_config(app_name) + models = app_config.get_models() + return models + except LookupError: + return [] + + +@login_required +@manager_can_enter("auth.view_permission") +def employee_permission_assign(request): + """ + This method is used to assign permissions to employee user + """ + + context = {} + template = "base/auth/permission.html" + if request.GET.get("profile_tab") and request.GET.get("employee_id"): + template = "tabs/group_permissions.html" + employees = Employee.objects.filter(id=request.GET["employee_id"]) + context["employee"] = employees.first() + else: + employees = Employee.objects.filter( + employee_user_id__user_permissions__isnull=False + ).distinct() + context["show_assign"] = True + permissions = [ + { + "app": app_name.capitalize().replace("_", " "), + "app_models": [ + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + for model in get_models_in_app(app_name) + if model._meta.model_name not in NO_PERMISSION_MODALS + ], + } + for app_name in APPS + ] + context["permissions"] = permissions + context["no_permission_models"] = NO_PERMISSION_MODALS + context["employees"] = paginator_qry(employees, request.GET.get("page")) + return render( + request, + template, + context, + ) + + +@login_required +@permission_required("view_permissions") +def employee_permission_search(request, codename=None, uid=None): + """ + This method renders template to view all instances of user permissions + """ + context = {} + template = "base/auth/permission_lines.html" + employees = EmployeeFilter(request.GET).qs + if request.GET.get("profile_tab"): + employees = Employee.objects.filter(id=request.GET["employee_id"]) + context["employee"] = employees.first() + else: + employees = employees.filter( + employee_user_id__user_permissions__isnull=False + ).distinct() + context["show_assign"] = True + permissions = [ + { + "app": app_name.capitalize().replace("_", " "), + "app_models": [ + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + for model in get_models_in_app(app_name) + if model._meta.model_name not in NO_PERMISSION_MODALS + ], + } + for app_name in APPS + ] + context["permissions"] = permissions + context["no_permission_models"] = NO_PERMISSION_MODALS + context["employees"] = paginator_qry(employees, request.GET.get("page")) + return render( + request, + template, + context, + ) + + +@login_required +@require_http_methods(["POST"]) +@permission_required("auth.add_permission") +def update_permission( + request, +): + """ + This method is used to remove user permission. + """ + form = AssignPermission(request.POST) + if form.is_valid(): + form.save() + return JsonResponse({"message": "Updated the permissions", "type": "success"}) + if ( + form.data.get("employee") + and Employee.objects.filter(id=form.data["employee"]).first() + ): + Employee.objects.filter( + id=form.data["employee"] + ).first().employee_user_id.user_permissions.clear() + return JsonResponse({"message": "All permission cleared", "type": "info"}) + return JsonResponse({"message": "Something went wrong", "type": "danger"}) + + +@login_required +@hx_request_required +@permission_required("auth.add_permission") +def permission_table(request): + """ + This method is used to render the permission table + """ + permissions = [] + apps = APPS + form = AssignPermission() + + no_permission_models = NO_PERMISSION_MODALS + + for app_name in apps: + app_models = [] + for model in get_models_in_app(app_name): + if model not in no_permission_models: + app_models.append( + { + "verbose_name": model._meta.verbose_name.capitalize(), + "model_name": model._meta.model_name, + } + ) + permissions.append( + {"app": app_name.capitalize().replace("_", " "), "app_models": app_models} + ) + if request.method == "POST": + form = AssignPermission(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Employee permission assigned.")) + return HttpResponse("") + return render( + request, + "base/auth/permission_assign.html", + { + "permissions": permissions, + "form": form, + "no_permission_models": no_permission_models, + }, + ) + + +@login_required +def work_type_request_view(request): + """ + This method renders template to view all work type requests + """ + previous_data = request.GET.urlencode() + employee = Employee.objects.filter(employee_user_id=request.user).first() + if request.user.has_perm("base.view_worktyperequest"): + work_type_requests = WorkTypeRequest.objects.all() + else: + work_type_requests = filtersubordinates( + request, WorkTypeRequest.objects.all(), "base.add_worktyperequest" + ) + work_type_requests = work_type_requests | WorkTypeRequest.objects.filter( + employee_id=employee + ) + work_type_requests = work_type_requests.filter(employee_id__is_active=True) + requests_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + work_type_requests, request.GET.get("page") + ).object_list + ] + ) + f = WorkTypeRequestFilter(request.GET, queryset=work_type_requests) + data_dict = parse_qs(previous_data) + get_key_instances(WorkTypeRequest, data_dict) + form = WorkTypeRequestForm() + form = choosesubordinates( + request, + form, + "base.add_worktypereqeust", + ) + form = include_employee_instance(request, form) + return render( + request, + "work_type_request/work_type_request_view.html", + { + "data": paginator_qry(f.qs, request.GET.get("page")), + "f": f, + "form": form, + "filter_dict": data_dict, + "requests_ids": requests_ids, + "gp_fields": WorkTypeRequestReGroup.fields, + }, + ) + + +@login_required +@manager_can_enter("base.view_worktyperequest") +def work_type_request_export(request): + if request.META.get("HTTP_HX_REQUEST") == "true": + export_filter = WorkTypeRequestFilter() + export_fields = WorkTypeRequestColumnForm() + context = { + "export_fields": export_fields, + "export_filter": export_filter, + } + return render( + request, "work_type_request/work_type_request_export.html", context=context + ) + return export_data( + request, + WorkTypeRequest, + WorkTypeRequestColumnForm, + WorkTypeRequestFilter, + "Work_type_request", + ) + + +@login_required +@hx_request_required +def work_type_request_search(request): + """ + This method is used to search work type request. + """ + employee = Employee.objects.filter(employee_user_id=request.user).first() + previous_data = request.GET.urlencode() + field = request.GET.get("field") + f = WorkTypeRequestFilter(request.GET) + work_typ_requests = ( + filtersubordinates(request, f.qs, "base.add_worktyperequest") + if not request.user.has_perm("base.view_worktyperequest") + else f.qs + ) + employee_work_requests = list(WorkTypeRequest.objects.filter(employee_id=employee)) + subordinates_work_requests = list(work_typ_requests) + combined_requests = list(set(subordinates_work_requests + employee_work_requests)) + combined_requests_qs = WorkTypeRequest.objects.filter( + id__in=[req.id for req in combined_requests] + ) + work_typ_requests = sortby(request, combined_requests_qs, "orderby") + template = "work_type_request/htmx/requests.html" + + if field != "" and field is not None: + work_typ_requests = group_by_queryset( + work_typ_requests, field, request.GET.get("page"), "page" + ) + list_values = [entry["list"] for entry in work_typ_requests] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + requests_ids = json.dumps(list(id_list)) + template = "work_type_request/htmx/group_by.html" + + else: + work_typ_requests = paginator_qry(work_typ_requests, request.GET.get("page")) + requests_ids = json.dumps( + [instance.id for instance in work_typ_requests.object_list] + ) + + data_dict = parse_qs(previous_data) + get_key_instances(WorkTypeRequest, data_dict) + return render( + request, + template, + { + "data": work_typ_requests, + "pd": previous_data, + "filter_dict": data_dict, + "requests_ids": requests_ids, + "field": field, + }, + ) + + +def handle_wtr_close_hx_url(request): + employee = request.user.employee_get.id + HTTP_REFERER = request.META.get("HTTP_REFERER", "") + previous_data = unquote(request.GET.urlencode().replace("pd=", "")) + close_hx_url = "" + close_hx_target = "" + + if HTTP_REFERER and "/" + "/".join(HTTP_REFERER.split("/")[3:]) == "/": + close_hx_url = reverse("dashboard-work-type-request") + close_hx_target = "#WorkTypeRequestApproveBody" + elif HTTP_REFERER and HTTP_REFERER.endswith("work-type-request-view/"): + close_hx_url = f"/work-type-request-search?{previous_data}" + close_hx_target = "#view-container" + elif HTTP_REFERER and HTTP_REFERER.endswith("employee-profile/"): + close_hx_url = f"/employee/shift-tab/{employee}?profile=true" + close_hx_target = "#shift_target" + elif HTTP_REFERER: + HTTP_REFERERS = [part for part in HTTP_REFERER.split("/") if part] + try: + employee_id = int(HTTP_REFERERS[-1]) + close_hx_url = f"/employee/shift-tab/{employee_id}" + close_hx_target = "#shift_target" + except ValueError: + pass + return close_hx_url, close_hx_target + + +@login_required +@hx_request_required +def work_type_request(request): + """ + This method is used to create request for work type . + """ + encoded_data = request.GET.urlencode() + previous_data = unquote(encoded_data.replace("pd=", "")) + form = WorkTypeRequestForm() + employee = request.user.employee_get.id + if request.GET.get("emp_id"): + employee = request.GET.get("emp_id") + form = WorkTypeRequestForm(initial={"employee_id": employee}) + form = choosesubordinates( + request, + form, + "base.add_worktyperequest", + ) + form = include_employee_instance(request, form) + + f = WorkTypeRequestFilter() + context = {"f": f, "pd": previous_data} + context["close_hx_url"], context["close_hx_target"] = handle_wtr_close_hx_url( + request + ) + if request.method == "POST": + form = WorkTypeRequestForm(request.POST) + form = choosesubordinates( + request, + form, + "base.add_worktyperequest", + ) + form = include_employee_instance(request, form) + if form.is_valid(): + instance = form.save() + try: + notify.send( + instance.employee_id, + recipient=( + instance.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ), + verb=f"You have new work type request to \ + validate for {instance.employee_id}", + verb_ar=f"لديك طلب نوع وظيفة جديد للتحقق من \ + {instance.employee_id}", + verb_de=f"Sie haben eine neue Arbeitstypanfrage zur \ + Validierung für {instance.employee_id}", + verb_es=f"Tiene una nueva solicitud de tipo de trabajo para \ + validar para {instance.employee_id}", + verb_fr=f"Vous avez une nouvelle demande de type de travail\ + à valider pour {instance.employee_id}", + icon="information", + redirect=reverse("work-type-request-view") + f"?id={instance.id}", + ) + except Exception as error: + pass + messages.success(request, _("Work type request added.")) + work_type_requests = WorkTypeRequest.objects.all() + if len(work_type_requests) == 1: + return HttpResponse("") + form = WorkTypeRequestForm() + context["form"] = form + return render(request, "work_type_request/request_form.html", context=context) + + +def handle_wtr_redirect(request, work_type_request): + hx_request = request.META.get("HTTP_HX_REQUEST") == "true" + if not hx_request: + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + current_url = "/" + "/".join( + request.META.get("HTTP_HX_CURRENT_URL", "").split("/")[3:] + ) + hx_target = request.META.get("HTTP_HX_TARGET") + + if not current_url: + return HttpResponse("") + + if hx_target == "objectDetailsModalTarget": + instances_ids = request.GET.get("instances_ids") + dashboard = request.GET.get("dashboard") + url = reverse( + "work-type-request-single-view", + kwargs={"obj_id": work_type_request.id}, + ) + return redirect(f"{url}?instances_ids={instances_ids}&dashboard={dashboard}") + + if current_url == "/": + return redirect(reverse("dashboard-work-type-request")) + + if "/work-type-request-view/" in current_url: + return redirect(f"/work-type-request-search?{request.GET.urlencode()}") + + if "/employee-view/" in current_url: + return redirect(f"/employee/shift-tab/{work_type_request.employee_id.id}") + + return HttpResponse("") + + +@login_required +def work_type_request_cancel(request, id): + """ + This method is used to cancel work type request + args: + id : work type request id + + """ + work_type_request = WorkTypeRequest.find(id) + if not work_type_request: + messages.error(request, _("Work type request not found.")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + if not ( + is_reportingmanger(request, work_type_request) + or request.user.has_perm("base.cancel_worktyperequest") + or work_type_request.employee_id == request.user.employee_get + and work_type_request.approved == False + ): + messages.error(request, _("You don't have permission")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + work_type_request.canceled = True + work_type_request.approved = False + work_info = EmployeeWorkInformation.objects.filter( + employee_id=work_type_request.employee_id + ) + if work_info.exists(): + work_type_request.employee_id.employee_work_info.work_type_id = ( + work_type_request.previous_work_type_id + ) + work_type_request.employee_id.employee_work_info.save() + work_type_request.save() + messages.success(request, _("Work type request has been rejected.")) + notify.send( + request.user.employee_get, + recipient=work_type_request.employee_id.employee_user_id, + verb="Your work type request has been rejected.", + verb_ar="تم إلغاء طلب نوع وظيفتك", + verb_de="Ihre Arbeitstypanfrage wurde storniert", + verb_es="Su solicitud de tipo de trabajo ha sido cancelada", + verb_fr="Votre demande de type de travail a été annulée", + redirect=reverse("work-type-request-view") + f"?id={work_type_request.id}", + icon="close", + ) + return handle_wtr_redirect(request, work_type_request) + + +@login_required +@manager_can_enter("base.change_worktyperequest") +def work_type_request_bulk_cancel(request): + """ + This method is used to cancel a bunch work type request + """ + ids = request.POST["ids"] + ids = json.loads(ids) + result = False + for id in ids: + work_type_request = WorkTypeRequest.objects.get(id=id) + if ( + is_reportingmanger(request, work_type_request) + or request.user.has_perm("base.cancel_worktyperequest") + or work_type_request.employee_id == request.user.employee_get + and work_type_request.approved == False + ): + work_type_request.canceled = True + work_type_request.approved = False + work_type_request.employee_id.employee_work_info.work_type_id = ( + work_type_request.previous_work_type_id + ) + work_type_request.employee_id.employee_work_info.save() + work_type_request.save() + messages.success(request, _("Work type request has been canceled.")) + notify.send( + request.user.employee_get, + recipient=work_type_request.employee_id.employee_user_id, + verb="Your work type request has been canceled.", + verb_ar="تم إلغاء طلب نوع وظيفتك.", + verb_de="Ihre Arbeitstypanfrage wurde storniert.", + verb_es="Su solicitud de tipo de trabajo ha sido cancelada.", + verb_fr="Votre demande de type de travail a été annulée.", + redirect=reverse("work-type-request-view") + + f"?id={work_type_request.id}", + icon="close", + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +def work_type_request_approve(request, id): + """ + This method is used to approve requested work type + """ + + work_type_request = WorkTypeRequest.find(id) + if not work_type_request: + messages.error(request, _("Work type request not found.")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + if not ( + is_reportingmanger(request, work_type_request) + or request.user.has_perm("approve_worktyperequest") + or request.user.has_perm("change_worktyperequest") + and not work_type_request.approved + ): + messages.error(request, _("You don't have permission")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + """ + Here the request will be approved, can send mail right here + """ + if not work_type_request.is_any_work_type_request_exists(): + work_type_request.approved = True + work_type_request.canceled = False + work_type_request.save() + messages.success(request, _("Work type request has been approved.")) + notify.send( + request.user.employee_get, + recipient=work_type_request.employee_id.employee_user_id, + verb="Your work type request has been approved.", + verb_ar="تمت الموافقة على طلب نوع وظيفتك.", + verb_de="Ihre Arbeitstypanfrage wurde genehmigt.", + verb_es="Su solicitud de tipo de trabajo ha sido aprobada.", + verb_fr="Votre demande de type de travail a été approuvée.", + redirect=reverse("work-type-request-view") + f"?id={work_type_request.id}", + icon="checkmark", + ) + else: + messages.error( + request, + _("An approved work type request already exists during this time period."), + ) + return handle_wtr_redirect(request, work_type_request) + + +@login_required +def work_type_request_bulk_approve(request): + """ + This method is used to approve bulk of requested work type + """ + ids = request.POST["ids"] + ids = json.loads(ids) + result = False + for id in ids: + work_type_request = WorkTypeRequest.objects.get(id=id) + if ( + is_reportingmanger(request, work_type_request) + or request.user.has_perm("approve_worktyperequest") + or request.user.has_perm("change_worktyperequest") + and not work_type_request.approved + ): + # """ + # Here the request will be approved, can send mail right here + # """ + work_type_request.approved = True + work_type_request.canceled = False + employee_work_info = work_type_request.employee_id.employee_work_info + employee_work_info.work_type_id = work_type_request.work_type_id + employee_work_info.save() + work_type_request.save() + messages.success(request, _("Work type request has been approved.")) + notify.send( + request.user.employee_get, + recipient=work_type_request.employee_id.employee_user_id, + verb="Your work type request has been approved.", + verb_ar="تمت الموافقة على طلب نوع وظيفتك.", + verb_de="Ihre Arbeitstypanfrage wurde genehmigt.", + verb_es="Su solicitud de tipo de trabajo ha sido aprobada.", + verb_fr="Votre demande de type de travail a été approuvée.", + redirect=reverse("work-type-request-view") + + f"?id={work_type_request.id}", + icon="checkmark", + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +@hx_request_required +@work_type_request_change_permission() +def work_type_request_update(request, work_type_request_id): + """ + This method is used to update work type request instance + args: + id : work type request instance id + + """ + work_type_request = WorkTypeRequest.objects.get(id=work_type_request_id) + form = WorkTypeRequestForm(instance=work_type_request) + form = choosesubordinates(request, form, "base.change_worktyperequest") + form = include_employee_instance(request, form) + if request.method == "POST": + response = render( + request, + "work_type_request/request_form.html", + { + "form": form, + }, + ) + form = WorkTypeRequestForm(request.POST, instance=work_type_request) + form = choosesubordinates(request, form, "base.change_worktyperequest") + form = include_employee_instance(request, form) + if form.is_valid(): + form.save() + messages.success(request, _("Request Updated Successfully")) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + + return render(request, "work_type_request/request_form.html", {"form": form}) + + +@login_required +@hx_request_required +@require_http_methods(["POST"]) +def work_type_request_delete(request, obj_id): + """ + This method is used to delete work type request + args: + id : work type request instance id + + """ + try: + work_type_request = WorkTypeRequest.objects.get(id=obj_id) + employee = work_type_request.employee_id + messages.success(request, _("Work type request deleted.")) + work_type_request.delete() + notify.send( + request.user.employee_get, + recipient=employee.employee_user_id, + verb="Your work type request has been deleted.", + verb_ar="تم حذف طلب نوع وظيفتك.", + verb_de="Ihre Arbeitstypanfrage wurde gelöscht.", + verb_es="Su solicitud de tipo de trabajo ha sido eliminada.", + verb_fr="Votre demande de type de travail a été supprimée.", + redirect="#", + icon="trash", + ) + except WorkTypeRequest.DoesNotExist: + employee = None + messages.error(request, _("Work type request not found.")) + except ProtectedError: + messages.error(request, _("You cannot delete this work type request.")) + hx_target = request.META.get("HTTP_HX_TARGET", None) + if hx_target and hx_target == "objectDetailsModalTarget": + instances_ids = request.GET.get("instances_ids") + instances_list = json.loads(instances_ids) + if obj_id in instances_list: + instances_list.remove(obj_id) + previous_instance, next_instance = closest_numbers( + json.loads(instances_ids), obj_id + ) + return redirect( + f"/work-type-request-single-view/{next_instance}/?instances_ids={instances_list}" + ) + elif hx_target and hx_target == "view-container": + previous_data = request.GET.urlencode() + work_type_requests = WorkTypeRequest.objects.all() + if work_type_requests.exists(): + return redirect(f"/work-type-request-search?{previous_data}") + else: + return HttpResponse("") + + elif hx_target and hx_target == "shift_target" and employee: + return redirect(f"/employee/shift-tab/{employee.id}") + else: + return HttpResponse("") + + +@login_required +def work_type_request_single_view(request, obj_id): + """ + This method is used to view details of an work type request + """ + work_type_request = WorkTypeRequest.objects.filter(id=obj_id).first() + context = { + "work_type_request": work_type_request, + "dashboard": request.GET.get("dashboard"), + } + requests_ids_json = request.GET.get("instances_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, obj_id) + context["requests_ids"] = requests_ids_json + context["previous"] = previous_id + context["next"] = next_id + context["close_hx_url"], context["close_hx_target"] = handle_wtr_close_hx_url( + request + ) + return render( + request, + "work_type_request/htmx/work_type_request_single_view.html", + context, + ) + + +@login_required +@permission_required("base.delete_worktyperequest") +@require_http_methods(["POST"]) +def work_type_request_bulk_delete(request): + """ + This method is used to delete work type request + args: + id : work type request instance id + + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for id in ids: + try: + work_type_request = WorkTypeRequest.objects.get(id=id) + user = work_type_request.employee_id.employee_user_id + work_type_request.delete() + messages.success(request, _("Work type request deleted.")) + notify.send( + request.user.employee_get, + recipient=user, + verb="Your work type request has been deleted.", + verb_ar="تم حذف طلب نوع وظيفتك.", + verb_de="Ihre Arbeitstypanfrage wurde gelöscht.", + verb_es="Su solicitud de tipo de trabajo ha sido eliminada.", + verb_fr="Votre demande de type de travail a été supprimée.", + redirect="#", + icon="trash", + ) + except WorkTypeRequest.DoesNotExist: + messages.error(request, _("Work type request not found.")) + except ProtectedError: + messages.error( + request, + _( + "You cannot delete {employee} work type request for the date {date}." + ).format( + employee=work_type_request.employee_id, + date=work_type_request.requested_date, + ), + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +@hx_request_required +def shift_request(request): + """ + This method is used to create shift request + """ + form = ShiftRequestForm() + employee = request.user.employee_get.id + if request.GET.get("emp_id"): + employee = request.GET.get("emp_id") + + form = ShiftRequestForm(initial={"employee_id": employee}) + form = choosesubordinates( + request, + form, + "base.add_shiftrequest", + ) + form = include_employee_instance(request, form) + f = ShiftRequestFilter() + if request.method == "POST": + form = ShiftRequestForm(request.POST) + form = choosesubordinates(request, form, "base.add_shiftrequest") + form = include_employee_instance(request, form) + response = render( + request, + "shift_request/htmx/shift_request_create_form.html", + {"form": form, "f": f}, + ) + if form.is_valid(): + instance = form.save() + try: + notify.send( + instance.employee_id, + recipient=( + instance.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ), + verb=f"You have new shift request to approve \ + for {instance.employee_id}", + verb_ar=f"لديك طلب وردية جديد للموافقة عليه لـ {instance.employee_id}", + verb_de=f"Sie müssen eine neue Schichtanfrage \ + für {instance.employee_id} genehmigen", + verb_es=f"Tiene una nueva solicitud de turno para \ + aprobar para {instance.employee_id}", + verb_fr=f"Vous avez une nouvelle demande de quart de\ + travail à approuver pour {instance.employee_id}", + icon="information", + redirect=reverse("shift-request-view") + f"?id={instance.id}", + ) + except Exception as e: + pass + messages.success(request, _("Shift request added")) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "shift_request/htmx/shift_request_create_form.html", + {"form": form, "f": f}, + ) + + +@login_required +def update_employee_allocation(request): + + shift = request.GET.get("shift_id") + form = ShiftAllocationForm() + shift = EmployeeShift.objects.filter(id=shift).first() + employee_ids = shift.employeeworkinformation_set.values_list( + "employee_id", flat=True + ) + employees = Employee.objects.filter(id__in=employee_ids) + form.fields["reallocate_to"].queryset = employees + context = {"form": form} + html_template = render_to_string( + "shift_request/htmx/shift_reallocate_employees.html", context + ) + return HttpResponse(html_template) + + +@login_required +@hx_request_required +def shift_request_allocation(request): + """ + This method is used to create shift request reallocation + """ + form = ShiftAllocationForm() + if request.GET.get("emp_id"): + employee = request.GET.get("emp_id") + form = ShiftAllocationForm(initial={"employee_id": employee}) + form = choosesubordinates( + request, + form, + "base.add_shiftrequest", + ) + form = include_employee_instance(request, form) + f = ShiftRequestFilter() + if request.method == "POST": + form = ShiftAllocationForm(request.POST) + form = choosesubordinates(request, form, "base.add_shiftrequest") + form = include_employee_instance(request, form) + response = render( + request, + "shift_request/htmx/shift_allocation_form.html", + {"form": form, "f": f}, + ) + if form.is_valid(): + instance = form.save() + reallocate_emp = form.cleaned_data["reallocate_to"] + try: + notify.send( + instance.employee_id, + recipient=( + instance.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ), + verb=f"You have a new shift reallocation request to approve for {instance.employee_id}.", + verb_ar=f"لديك طلب تخصيص جديد للورديات يتعين عليك الموافقة عليه لـ {instance.employee_id}.", + verb_de=f"Sie haben eine neue Anfrage zur Verschiebung der Schichtzuteilung zur Genehmigung für {instance.employee_id}.", + verb_es=f"Tienes una nueva solicitud de reasignación de turnos para aprobar para {instance.employee_id}.", + verb_fr=f"Vous avez une nouvelle demande de réaffectation de shift à approuver pour {instance.employee_id}.", + icon="information", + redirect=reverse("shift-request-view") + f"?id={instance.id}", + ) + except Exception as e: + pass + + try: + notify.send( + instance.employee_id, + recipient=reallocate_emp, + verb=f"You have a new shift reallocation request from {instance.employee_id}.", + verb_ar=f"لديك طلب تخصيص جديد للورديات من {instance.employee_id}.", + verb_de=f"Sie haben eine neue Anfrage zur Verschiebung der Schichtzuteilung von {instance.employee_id}.", + verb_es=f"Tienes una nueva solicitud de reasignación de turnos de {instance.employee_id}.", + verb_fr=f"Vous avez une nouvelle demande de réaffectation de shift de {instance.employee_id}.", + icon="information", + redirect=reverse("shift-request-view") + f"?id={instance.id}", + ) + except Exception as e: + pass + + messages.success(request, _("Request Added")) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "shift_request/htmx/shift_allocation_form.html", + {"form": form, "f": f}, + ) + + +@login_required +def shift_request_view(request): + """ + This method renders all shift request instances to a template + """ + previous_data = request.GET.urlencode() + employee = Employee.objects.filter(employee_user_id=request.user).first() + shift_requests = filtersubordinates( + request, + ShiftRequest.objects.filter(reallocate_to__isnull=True), + "base.view_shiftrequest", + ) + shift_requests = shift_requests | ShiftRequest.objects.filter(employee_id=employee) + shift_requests = shift_requests.filter(employee_id__is_active=True) + + allocated_shift_requests = filtersubordinates( + request, + ShiftRequest.objects.filter(reallocate_to__isnull=False), + "base.view_shiftrequest", + ) + allocated_requests = ShiftRequest.objects.filter(reallocate_to__isnull=False) + if not request.user.has_perm("base.view_shiftrequest"): + allocated_requests = allocated_requests.filter( + Q(reallocate_to=employee) | Q(employee_id=employee) + ) + allocated_shift_requests = allocated_shift_requests | allocated_requests + + requests_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + shift_requests, request.GET.get("page") + ).object_list + ] + ) + allocated_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + allocated_shift_requests, request.GET.get("page") + ).object_list + ] + ) + f = ShiftRequestFilter(request.GET, queryset=shift_requests) + data_dict = parse_qs(previous_data) + get_key_instances(ShiftRequest, data_dict) + form = ShiftRequestForm() + + form = choosesubordinates( + request, + form, + "base.add_shiftrequest", + ) + form = include_employee_instance(request, form) + return render( + request, + "shift_request/shift_request_view.html", + { + "allocated_data": paginator_qry( + allocated_shift_requests, request.GET.get("page") + ), + "data": paginator_qry(f.qs, request.GET.get("page")), + "f": f, + "form": form, + "filter_dict": data_dict, + "requests_ids": requests_ids, + "allocated_ids": allocated_ids, + "gp_fields": ShiftRequestReGroup.fields, + }, + ) + + +@login_required +@manager_can_enter("base.view_shiftrequest") +def shift_request_export(request): + if request.META.get("HTTP_HX_REQUEST") == "true": + export_fields = ShiftRequestColumnForm() + export_filter = ShiftRequestFilter() + + context = { + "export_fields": export_fields, + "export_filter": export_filter, + } + return render( + request, "shift_request/shift_request_export.html", context=context + ) + return export_data( + request, + ShiftRequest, + ShiftRequestColumnForm, + ShiftRequestFilter, + "Shift_requests", + ) + + +@login_required +def shift_request_search(request): + """ + This method is used search shift request by employee and also used to filter shift request. + """ + + employee = Employee.objects.filter(employee_user_id=request.user).first() + previous_data = request.GET.urlencode() + field = request.GET.get("field") + f = ShiftRequestFilter(request.GET) + f = sortby(request, f.qs, "sortby") + shift_requests = filtersubordinates( + request, f.filter(reallocate_to__isnull=True), "base.add_shiftrequest" + ) + shift_requests = shift_requests | f.filter( + employee_id__employee_user_id=request.user + ) + + allocated_shift_requests = filtersubordinates( + request, f.filter(reallocate_to__isnull=False), "base.add_shiftrequest" + ) + if not request.user.has_perm("base.view_shiftrequest"): + allocated_shift_requests = allocated_shift_requests | f.filter( + Q(reallocate_to=employee) | Q(employee_id=employee) + ) + + requests_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + shift_requests, request.GET.get("page") + ).object_list + ] + ) + + allocated_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + allocated_shift_requests, request.GET.get("page") + ).object_list + ] + ) + + data_dict = parse_qs(previous_data) + template = "shift_request/htmx/requests.html" + if field != "" and field is not None: + shift_requests = group_by_queryset( + shift_requests, field, request.GET.get("page"), "page" + ) + allocated_shift_requests = group_by_queryset( + allocated_shift_requests, field, request.GET.get("page"), "page" + ) + shift_list_values = [entry["list"] for entry in shift_requests] + allocated_list_values = [entry["list"] for entry in allocated_shift_requests] + shift_id_list = [] + allocated_id_list = [] + + for value in shift_list_values: + for instance in value.object_list: + shift_id_list.append(instance.id) + + for value in allocated_list_values: + for instance in value.object_list: + allocated_id_list.append(instance.id) + + requests_ids = json.dumps(list(shift_id_list)) + allocated_ids = json.dumps(list(allocated_id_list)) + template = "shift_request/htmx/group_by.html" + + else: + shift_requests = paginator_qry(shift_requests, request.GET.get("page")) + allocated_shift_requests = paginator_qry( + allocated_shift_requests, request.GET.get("page") + ) + requests_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + shift_requests, request.GET.get("page") + ).object_list + ] + ) + + allocated_ids = json.dumps( + [ + instance.id + for instance in paginator_qry( + allocated_shift_requests, request.GET.get("page") + ).object_list + ] + ) + + get_key_instances(ShiftRequest, data_dict) + return render( + request, + template, + { + "allocated_data": allocated_shift_requests, + "data": paginator_qry(shift_requests, request.GET.get("page")), + "pd": previous_data, + "filter_dict": data_dict, + "requests_ids": requests_ids, + "allocated_ids": allocated_ids, + "field": field, + }, + ) + + +@login_required +@hx_request_required +def shift_request_details(request, id): + """ + This method is used to show shift request details in a modal + args: + id : shift request instance id + """ + shift_request = ShiftRequest.find(id) + requests_ids_json = request.GET.get("instances_ids") + context = { + "shift_request": shift_request, + "dashboard": request.GET.get("dashboard"), + } + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, id) + context["previous"] = previous_id + context["next"] = next_id + context["requests_ids"] = requests_ids_json + return render( + request, + "shift_request/htmx/shift_request_detail.html", + context, + ) + + +@login_required +@hx_request_required +def shift_allocation_request_details(request, id): + """ + This method is used to show shift request details in a modal + args: + id : shift request instance id + """ + shift_request = ShiftRequest.find(id) + requests_ids_json = request.GET.get("instances_ids") + context = { + "shift_request": shift_request, + "dashboard": request.GET.get("dashboard"), + } + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, id) + context["previous"] = previous_id + context["next"] = next_id + context["allocation_ids"] = requests_ids_json + return render( + request, + "shift_request/htmx/allocation_details.html", + context, + ) + + +@login_required +@hx_request_required +@shift_request_change_permission() +def shift_request_update(request, shift_request_id): + """ + This method is used to update shift request instance + args: + id : shift request instance id + """ + shift_request = ShiftRequest.objects.get(id=shift_request_id) + form = ShiftRequestForm(instance=shift_request) + form = choosesubordinates(request, form, "base.change_shiftrequest") + form = include_employee_instance(request, form) + if request.method == "POST": + if not shift_request.approved: + response = render( + request, + "shift_request/request_update_form.html", + { + "form": form, + }, + ) + form = ShiftRequestForm(request.POST, instance=shift_request) + form = choosesubordinates(request, form, "base.change_shiftrequest") + form = include_employee_instance(request, form) + if form.is_valid(): + form.save() + messages.success(request, _("Request Updated Successfully")) + return HttpResponse( + response.content.decode("utf-8") + + "" + ) + else: + messages.info(request, _("Can't edit approved shift request")) + return HttpResponse("") + + return render(request, "shift_request/request_update_form.html", {"form": form}) + + +@login_required +def shift_allocation_request_update(request, shift_request_id): + """ + This method is used to update shift request instance + args: + id : shift request instance id + """ + shift_request = ShiftRequest.objects.get(id=shift_request_id) + form = ShiftAllocationForm(instance=shift_request) + form = choosesubordinates(request, form, "base.change_shiftrequest") + form = include_employee_instance(request, form) + if request.method == "POST": + response = render( + request, + "shift_request/request_update_form.html", + { + "form": form, + }, + ) + form = ShiftAllocationForm(request.POST, instance=shift_request) + form = choosesubordinates(request, form, "base.change_shiftrequest") + form = include_employee_instance(request, form) + if form.is_valid(): + form.save() + instance = form.save() + reallocate_emp = form.cleaned_data["reallocate_to"] + try: + notify.send( + instance.employee_id, + recipient=( + instance.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ), + verb=f"You have a new shift reallocation request to approve for {instance.employee_id}.", + verb_ar=f"لديك طلب تخصيص جديد للورديات يتعين عليك الموافقة عليه لـ {instance.employee_id}.", + verb_de=f"Sie haben eine neue Anfrage zur Verschiebung der Schichtzuteilung zur Genehmigung für {instance.employee_id}.", + verb_es=f"Tienes una nueva solicitud de reasignación de turnos para aprobar para {instance.employee_id}.", + verb_fr=f"Vous avez une nouvelle demande de réaffectation de shift à approuver pour {instance.employee_id}.", + icon="information", + redirect=reverse("shift-request-view") + f"?id={instance.id}", + ) + except Exception as e: + pass + + try: + notify.send( + instance.employee_id, + recipient=reallocate_emp, + verb=f"You have a new shift reallocation request from {instance.employee_id}.", + verb_ar=f"لديك طلب تخصيص جديد للورديات من {instance.employee_id}.", + verb_de=f"Sie haben eine neue Anfrage zur Verschiebung der Schichtzuteilung von {instance.employee_id}.", + verb_es=f"Tienes una nueva solicitud de reasignación de turnos de {instance.employee_id}.", + verb_fr=f"Vous avez une nouvelle demande de réaffectation de shift de {instance.employee_id}.", + icon="information", + redirect=reverse("shift-request-view") + f"?id={instance.id}", + ) + except Exception as e: + pass + messages.success(request, _("Request Updated Successfully")) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + + return render( + request, "shift_request/allocation_request_update_form.html", {"form": form} + ) + + +@login_required +def shift_request_cancel(request, id): + """ + This method is used to update or cancel shift request + args: + id : shift request id + + """ + + shift_request = ShiftRequest.find(id) + if not shift_request: + messages.error(request, _("Shift request not found.")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + if not ( + is_reportingmanger(request, shift_request) + or request.user.has_perm("base.cancel_shiftrequest") + or shift_request.employee_id == request.user.employee_get + and shift_request.approved == False + ): + messages.error(request, _("You don't have permission")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + today_date = datetime.today().date() + if ( + shift_request.approved + and shift_request.requested_date <= today_date <= shift_request.requested_till + and not shift_request.is_permanent_shift + ): + shift_request.employee_id.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + shift_request.employee_id.employee_work_info.save() + shift_request.canceled = True + shift_request.approved = False + work_info = EmployeeWorkInformation.objects.filter( + employee_id=shift_request.employee_id + ) + if work_info.exists(): + shift_request.employee_id.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + if shift_request.reallocate_to and work_info.exists(): + shift_request.reallocate_to.employee_work_info.shift_id = shift_request.shift_id + shift_request.reallocate_to.employee_work_info.save() + if work_info.exists(): + shift_request.employee_id.employee_work_info.save() + shift_request.save() + messages.success(request, _("Shift request rejected")) + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb="Your shift request has been canceled.", + verb_ar="تم إلغاء طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde storniert.", + verb_es="Se ha cancelado su solicitud de turno.", + verb_fr="Votre demande de quart a été annulée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="close", + ) + if shift_request.reallocate_to: + notify.send( + request.user.employee_get, + recipient=shift_request.reallocate_to.employee_user_id, + verb="Your shift request has been rejected.", + verb_ar="تم إلغاء طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde storniert.", + verb_es="Se ha cancelado su solicitud de turno.", + verb_fr="Votre demande de quart a été annulée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="close", + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +def shift_allocation_request_cancel(request, id): + """ + This method is used to update or cancel shift request + args: + id : shift request id + + """ + + shift_request = ShiftRequest.find(id) + + shift_request.reallocate_canceled = True + shift_request.reallocate_approved = False + work_info = EmployeeWorkInformation.objects.filter( + employee_id=shift_request.employee_id + ) + if work_info.exists(): + shift_request.employee_id.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + shift_request.employee_id.employee_work_info.save() + shift_request.save() + messages.success(request, _("Shift request canceled")) + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb="Your shift request has been canceled.", + verb_ar="تم إلغاء طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde storniert.", + verb_es="Se ha cancelado su solicitud de turno.", + verb_fr="Votre demande de quart a été annulée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="close", + ) + + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@manager_can_enter("base.change_shiftrequest") +@require_http_methods(["POST"]) +def shift_request_bulk_cancel(request): + """ + This method is used to cancel a bunch of shift request. + """ + ids = request.POST["ids"] + ids = json.loads(ids) + result = False + for id in ids: + shift_request = ShiftRequest.objects.get(id=id) + if ( + is_reportingmanger(request, shift_request) + or request.user.has_perm("base.cancel_shiftrequest") + or shift_request.employee_id == request.user.employee_get + and shift_request.approved == False + ): + shift_request.canceled = True + shift_request.approved = False + shift_request.employee_id.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + + if shift_request.reallocate_to: + shift_request.reallocate_to.employee_work_info.shift_id = ( + shift_request.shift_id + ) + shift_request.reallocate_to.employee_work_info.save() + + shift_request.employee_id.employee_work_info.save() + shift_request.save() + messages.success(request, _("Shift request canceled")) + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb="Your shift request has been canceled.", + verb_ar="تم إلغاء طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde storniert.", + verb_es="Se ha cancelado su solicitud de turno.", + verb_fr="Votre demande de quart a été annulée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="close", + ) + if shift_request.reallocate_to: + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb="Your shift request has been canceled.", + verb_ar="تم إلغاء طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde storniert.", + verb_es="Se ha cancelado su solicitud de turno.", + verb_fr="Votre demande de quart a été annulée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="close", + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +@manager_can_enter("base.change_shiftrequest") +def shift_request_approve(request, id): + """ + Approve shift request + args: + id : shift request instance id + """ + + shift_request = ShiftRequest.find(id) + if not shift_request: + messages.error(request, _("Shift request not found.")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + user = request.user + if not ( + is_reportingmanger(request, shift_request) + or user.has_perm("approve_shiftrequest") + or user.has_perm("change_shiftrequest") + and not shift_request.approved + ): + messages.error(request, _("You don't have permission")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + if shift_request.is_any_request_exists(): + messages.error( + request, + _("An approved shift request already exists during this time period."), + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + today_date = datetime.today().date() + if not shift_request.is_permanent_shift: + if shift_request.requested_date <= today_date <= shift_request.requested_till: + shift_request.employee_id.employee_work_info.shift_id = ( + shift_request.shift_id + ) + shift_request.employee_id.employee_work_info.save() + shift_request.approved = True + shift_request.canceled = False + + if shift_request.reallocate_to: + shift_request.reallocate_to.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + shift_request.reallocate_to.employee_work_info.save() + + shift_request.save() + messages.success(request, _("Shift has been approved.")) + + recipients = [shift_request.employee_id.employee_user_id] + if shift_request.reallocate_to: + recipients.append(shift_request.reallocate_to.employee_user_id) + + for recipient in recipients: + notify.send( + user.employee_get, + recipient=recipient, + verb="Your shift request has been approved.", + verb_ar="تمت الموافقة على طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde genehmigt.", + verb_es="Se ha aprobado su solicitud de turno.", + verb_fr="Votre demande de quart a été approuvée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="checkmark", + ) + + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +def shift_allocation_request_approve(request, id): + """ + This method is used to approve shift request + args: + id : shift request instance id + """ + + shift_request = ShiftRequest.find(id) + + if not shift_request.is_any_request_exists(): + shift_request.reallocate_approved = True + shift_request.reallocate_canceled = False + shift_request.save() + messages.success(request, _("You are available for shift reallocation.")) + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb=f"{request.user.employee_get} is available for shift reallocation.", + verb_ar=f"{request.user.employee_get} متاح لإعادة توزيع الورديات.", + verb_de=f"{request.user.employee_get} steht für die Verschiebung der Schichtzuteilung zur Verfügung.", + verb_es=f"{request.user.employee_get} está disponible para la reasignación de turnos.", + verb_fr=f"{request.user.employee_get} est disponible pour la réaffectation de shift.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="checkmark", + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + else: + messages.error( + request, + _("An approved shift request already exists during this time period."), + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@require_http_methods(["POST"]) +@manager_can_enter("base.change_shiftrequest") +def shift_request_bulk_approve(request): + """ + This method is used to approve a bunch of shift request + """ + ids = request.POST["ids"] + ids = json.loads(ids) + result = False + for id in ids: + shift_request = ShiftRequest.objects.get(id=id) + if ( + is_reportingmanger(request, shift_request) + or request.user.has_perm("approve_shiftrequest") + or request.user.has_perm("change_shiftrequest") + and not shift_request.approved + ): + """ + here the request will be approved, can send mail right here + """ + shift_request.approved = True + shift_request.canceled = False + + if shift_request.reallocate_to: + shift_request.reallocate_to.employee_work_info.shift_id = ( + shift_request.previous_shift_id + ) + shift_request.reallocate_to.employee_work_info.save() + + employee_work_info = shift_request.employee_id.employee_work_info + employee_work_info.shift_id = shift_request.shift_id + employee_work_info.save() + shift_request.save() + messages.success(request, _("Shifts have been approved.")) + notify.send( + request.user.employee_get, + recipient=shift_request.employee_id.employee_user_id, + verb="Your shift request has been approved.", + verb_ar="تمت الموافقة على طلبك للوردية.", + verb_de="Ihr Schichtantrag wurde genehmigt.", + verb_es="Se ha aprobado su solicitud de turno.", + verb_fr="Votre demande de quart a été approuvée.", + redirect=reverse("shift-request-view") + f"?id={shift_request.id}", + icon="checkmark", + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +@require_http_methods(["POST"]) +def shift_request_delete(request, id): + """ + This method is used to delete shift request instance + args: + id : shift request instance id + + """ + try: + shift_request = ShiftRequest.find(id) + user = shift_request.employee_id.employee_user_id + messages.success(request, "Shift request deleted") + shift_request.delete() + notify.send( + request.user.employee_get, + recipient=user, + verb="Your shift request has been deleted.", + verb_ar="تم حذف طلب الوردية الخاص بك.", + verb_de="Ihr Schichtantrag wurde gelöscht.", + verb_es="Se ha eliminado su solicitud de turno.", + verb_fr="Votre demande de quart a été supprimée.", + redirect="#", + icon="trash", + ) + + except ShiftRequest.DoesNotExist: + messages.error(request, _("Shift request not found.")) + except ProtectedError: + messages.error(request, _("You cannot delete this shift request.")) + + hx_target = request.META.get("HTTP_HX_TARGET", None) + if hx_target and hx_target == "shift_target" and shift_request.employee_id: + return redirect(f"/employee/shift-tab/{shift_request.employee_id.id}") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@permission_required("delete_shiftrequest") +@require_http_methods(["POST"]) +def shift_request_bulk_delete(request): + """ + This method is used to delete shift request instance + args: + id : shift request instance id + + """ + ids = request.POST["ids"] + ids = json.loads(ids) + result = False + for id in ids: + try: + shift_request = ShiftRequest.objects.get(id=id) + user = shift_request.employee_id.employee_user_id + shift_request.delete() + messages.success(request, _("Shift request deleted.")) + notify.send( + request.user.employee_get, + recipient=user, + verb="Your shift request has been deleted.", + verb_ar="تم حذف طلب الوردية الخاص بك.", + verb_de="Ihr Schichtantrag wurde gelöscht.", + verb_es="Se ha eliminado su solicitud de turno.", + verb_fr="Votre demande de quart a été supprimée.", + redirect="#", + icon="trash", + ) + except ShiftRequest.DoesNotExist: + messages.error(request, _("Shift request not found.")) + except ProtectedError: + messages.error( + request, + _( + "You cannot delete {employee} shift request for the date {date}." + ).format( + employee=shift_request.employee_id, + date=shift_request.requested_date, + ), + ) + result = True + return JsonResponse({"result": result}) + + +@login_required +def notifications(request): + """ + This method will render notification items + """ + all_notifications = request.user.notifications.unread() + return render( + request, + "notification/notification_items.html", + {"notifications": all_notifications}, + ) + + +@login_required +def clear_notification(request): + """ + This method is used to clear notification + """ + try: + request.user.notifications.unread().delete() + messages.success(request, _("Unread notifications removed.")) + except Exception as e: + messages.error(request, e) + notifications = request.user.notifications.unread() + return render( + request, + "notification/notification_items.html", + {"notifications": notifications}, + ) + + +@login_required +def delete_all_notifications(request): + try: + request.user.notifications.read().delete() + request.user.notifications.unread().delete() + messages.success(request, _("All notifications removed.")) + except Exception as e: + messages.error(request, e) + notifications = request.user.notifications.all() + return render( + request, "notification/all_notifications.html", {"notifications": notifications} + ) + + +@login_required +def delete_notification(request, id): + """ + This method is used to delete notification + """ + script = "" + try: + request.user.notifications.get(id=id).delete() + messages.success(request, _("Notification deleted.")) + except Exception as e: + messages.error(request, e) + if not request.user.notifications.all(): + script = """""" + return HttpResponse(script) + + +@login_required +def mark_as_read_notification(request, notification_id): + script = "" + notification = Notification.objects.get(id=notification_id) + notification.mark_as_read() + if not request.user.notifications.unread(): + script = """""" + return HttpResponse(script) + + +@login_required +def mark_as_read_notification_json(request): + try: + notification_id = request.POST["notification_id"] + notification_id = int(notification_id) + notification = Notification.objects.get(id=notification_id) + notification.mark_as_read() + return JsonResponse({"success": True}) + except: + return JsonResponse({"success": False, "error": "Invalid request"}) + + +@login_required +def read_notifications(request): + """ + This method is to mark as read the notification + """ + try: + request.user.notifications.all().mark_all_as_read() + messages.info(request, _("Notifications marked as read")) + except Exception as e: + messages.error(request, e) + notifications = request.user.notifications.unread() + + return render( + request, + "notification/notification_items.html", + {"notifications": notifications}, + ) + + +@login_required +def all_notifications(request): + """ + This method to render all notifications to template + """ + return render( + request, + "notification/all_notifications.html", + {"notifications": request.user.notifications.all()}, + ) + + +@login_required +def general_settings(request): + """ + This method is used to render settings template + """ + if apps.is_installed("payroll"): + PayrollSettings = get_horilla_model_class( + app_label="payroll", model="payrollsettings" + ) + EncashmentGeneralSettings = get_horilla_model_class( + app_label="payroll", model="encashmentgeneralsettings" + ) + from payroll.forms.component_forms import PayrollSettingsForm + from payroll.forms.forms import EncashmentGeneralSettingsForm + + currency_instance = PayrollSettings.objects.first() + currency_form = PayrollSettingsForm(instance=currency_instance) + encashment_instance = EncashmentGeneralSettings.objects.first() + encashment_form = EncashmentGeneralSettingsForm(instance=encashment_instance) + else: + encashment_form = None + currency_form = None + + selected_company_id = request.session.get("selected_company") + + if selected_company_id == "all" or not selected_company_id: + companies = Company.objects.all() + else: + companies = Company.objects.filter(id=selected_company_id) + + # Fetch or create EmployeeGeneralSetting instance + prefix_instance = EmployeeGeneralSetting.objects.first() + prefix_form = EmployeeGeneralSettingPrefixForm(instance=prefix_instance) + instance = AnnouncementExpire.objects.first() + form = AnnouncementExpireForm(instance=instance) + enabled_block_unblock = ( + AccountBlockUnblock.objects.exists() + and AccountBlockUnblock.objects.first().is_enabled + ) + enabled_profile_edit = ( + ProfileEditFeature.objects.exists() + and ProfileEditFeature.objects.first().is_enabled + ) + history_tracking_instance = HistoryTrackingFields.objects.first() + history_fields_form_initial = {} + if history_tracking_instance and history_tracking_instance.tracking_fields: + history_fields_form_initial = { + "tracking_fields": history_tracking_instance.tracking_fields[ + "tracking_fields" + ] + } + history_fields_form = HistoryTrackingFieldsForm(initial=history_fields_form_initial) + + if DynamicPagination.objects.filter(user_id=request.user).exists(): + pagination = DynamicPagination.objects.filter(user_id=request.user).first() + pagination_form = DynamicPaginationForm(instance=pagination) + else: + pagination_form = DynamicPaginationForm() + if request.method == "POST": + form = AnnouncementExpireForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, _("Settings updated.")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + return render( + request, + "base/general_settings.html", + { + "form": form, + "currency_form": currency_form, + "pagination_form": pagination_form, + "encashment_form": encashment_form, + "history_fields_form": history_fields_form, + "history_tracking_instance": history_tracking_instance, + "enabled_block_unblock": enabled_block_unblock, + "enabled_profile_edit": enabled_profile_edit, + "prefix_form": prefix_form, + "companies": companies, + "selected_company_id": selected_company_id, + }, + ) + + +@login_required +@permission_required("base.view_company") +def date_settings(request): + """ + This method is used to render Date format selector in settings + """ + return render(request, "base/company/date.html") + + +@permission_required("base.change_company") +@csrf_exempt # Use this decorator if CSRF protection is enabled +def save_date_format(request): + if request.method == "POST": + # Taking the selected Date Format + selected_format = request.POST.get("selected_format") + + if not len(selected_format): + messages.error(request, _("Please select a valid date format.")) + else: + user = request.user + employee = user.employee_get + if request.user.is_superuser: + selected_company = request.session.get("selected_company") + if selected_company == "all": + all_companies = Company.objects.all() + for cmp in all_companies: + cmp.date_format = selected_format + cmp.save() + messages.success(request, _("Date format saved successfully.")) + else: + company = Company.objects.get(id=selected_company) + company.date_format = selected_format + company.save() + messages.success(request, _("Date format saved successfully.")) + + # Return a JSON response indicating success + return JsonResponse({"success": True}) + else: + # Taking the company_name of the user + info = EmployeeWorkInformation.objects.filter(employee_id=employee) + # Employee workinformation will not exists if he/she chnged the company, So can't save the date format. + if info.exists(): + for data in info: + employee_company = data.company_id + + company_name = Company.objects.filter(company=employee_company) + emp_company = company_name.first() + + if emp_company is None: + messages.warning( + request, _("Please update the company field for the user.") + ) + else: + # Save the selected format to the backend + emp_company.date_format = selected_format + emp_company.save() + messages.success(request, _("Date format saved successfully.")) + else: + messages.warning( + request, + _("Date format cannot saved. You are not in the company."), + ) + # Return a JSON response indicating success + return JsonResponse({"success": True}) + + # Return a JSON response for unsupported methods + return JsonResponse({"error": False, "error": "Unsupported method"}, status=405) + + +@login_required +def get_date_format(request): + user = request.user + employee = user.employee_get + + selected_company = request.session.get("selected_company") + if selected_company != "all" and request.user.is_superuser: + company = Company.objects.get(id=selected_company) + date_format = company.date_format + if date_format: + date_format = date_format + else: + date_format = "MMM. D, YYYY" + return JsonResponse({"selected_format": date_format}) + + # Taking the company_name of the user + info = EmployeeWorkInformation.objects.filter(employee_id=employee) + if info.exists(): + for data in info: + employee_company = data.company_id + company_name = Company.objects.filter(company=employee_company) + emp_company = company_name.first() + if emp_company: + # Access the date_format attribute directly + date_format = emp_company.date_format if emp_company else "MMM. D, YYYY" + else: + date_format = "MMM. D, YYYY" + else: + date_format = "MMM. D, YYYY" + # Return the date format as JSON response + return JsonResponse({"selected_format": date_format}) + + +@permission_required("base.change_company") +@csrf_exempt # Use this decorator if CSRF protection is enabled +def save_time_format(request): + if request.method == "POST": + # Taking the selected Time Format + selected_format = request.POST.get("selected_format") + + if not len(selected_format): + messages.error(request, _("Please select a valid time format.")) + else: + user = request.user + employee = user.employee_get + if request.user.is_superuser: + selected_company = request.session.get("selected_company") + if selected_company == "all": + all_companies = Company.objects.all() + for cmp in all_companies: + cmp.time_format = selected_format + cmp.save() + messages.success(request, _("Date format saved successfully.")) + else: + company = Company.objects.get(id=selected_company) + company.time_format = selected_format + company.save() + messages.success(request, _("Date format saved successfully.")) + + # Return a JSON response indicating success + return JsonResponse({"success": True}) + else: + # Taking the company_name of the user + info = EmployeeWorkInformation.objects.filter(employee_id=employee) + # Employee workinformation will not exists if he/she chnged the company, So can't save the time format. + if info.exists(): + for data in info: + employee_company = data.company_id + + company_name = Company.objects.filter(company=employee_company) + emp_company = company_name.first() + + if emp_company is None: + messages.warning( + request, _("Please update the company field for the user.") + ) + else: + # Save the selected format to the backend + emp_company.time_format = selected_format + emp_company.save() + messages.success(request, _("Time format saved successfully.")) + else: + messages.warning( + request, + _("Time format cannot saved. You are not in the company."), + ) + + # Return a JSON response indicating success + return JsonResponse({"success": True}) + + # Return a JSON response for unsupported methods + return JsonResponse({"error": False, "error": "Unsupported method"}, status=405) + + +@login_required +def get_time_format(request): + user = request.user + employee = user.employee_get + + selected_company = request.session.get("selected_company") + if selected_company != "all" and request.user.is_superuser: + company = Company.objects.get(id=selected_company) + time_format = company.time_format + if time_format: + time_format = time_format + else: + time_format = "hh:mm A" + return JsonResponse({"selected_format": time_format}) + + # Taking the company_name of the user + info = EmployeeWorkInformation.objects.filter(employee_id=employee) + if info.exists(): + for data in info: + employee_company = data.company_id + company_name = Company.objects.filter(company=employee_company) + emp_company = company_name.first() + if emp_company: + # Access the date_format attribute directly + time_format = emp_company.time_format + else: + time_format = "hh:mm A" + else: + time_format = "hh:mm A" + # Return the date format as JSON response + return JsonResponse({"selected_format": time_format}) + + +@login_required +def history_field_settings(request): + if request.method == "POST": + fields = request.POST.getlist("tracking_fields") + check = request.POST.get("work_info_track") + history_object, created = HistoryTrackingFields.objects.get_or_create( + pk=1, defaults={"tracking_fields": {"tracking_fields": fields}} + ) + + if not created: + history_object.tracking_fields = {"tracking_fields": fields} + if check == "on": + history_object.work_info_track = True + else: + history_object.work_info_track = False + messages.success(request, _("Settings updated.")) + history_object.save() + + return redirect(general_settings) + + +@login_required +@permission_required("horilla_audit.change_accountblockunblock") +def enable_account_block_unblock(request): + if request.method == "POST": + enabled = request.POST.get("enable_block_account") == "on" + instance = AccountBlockUnblock.objects.first() + if instance: + instance.is_enabled = enabled + instance.save() + else: + AccountBlockUnblock.objects.create(is_enabled=enabled) + messages.success( + request, + _( + f"Account block/unblock setting has been {'enabled' if enabled else 'disabled'}." + ), + ) + if request.META.get("HTTP_HX_REQUEST"): + return HttpResponse() + return redirect(general_settings) + return HttpResponse(status=405) + + +@login_required +@permission_required("employee.change_employee") +def enable_profile_edit_feature(request): + + if request.method == "POST": + enabled = request.POST.get("enable_profile_edit") == "on" + instance = ProfileEditFeature.objects.first() + feature = DefaultAccessibility.objects.filter(feature="profile_edit").first() + if instance: + instance.is_enabled = enabled + instance.save() + else: + ProfileEditFeature.objects.create(is_enabled=enabled) + + if enabled and not feature: + DefaultAccessibility.objects.create( + feature="profile_edit", filter={"feature": ["profile_edit"]} + ) + else: + if feature is not None: + feature.delete() + messages.info( + request, _("Profile edit accessibility feature has been removed.") + ) + + if enabled: + if not any(item[0] == "profile_edit" for item in ACCESSBILITY_FEATURE): + ACCESSBILITY_FEATURE.append(("profile_edit", _("Profile Edit Access"))) + else: + ACCESSBILITY_FEATURE.pop() + + messages.success( + request, + _(f"Profile edit feature has been {'enabled' if enabled else 'disabled'}."), + ) + if request.META.get("HTTP_HX_REQUEST"): + return HttpResponse() + return redirect(general_settings) + return HttpResponse(status=405) + + +@login_required +def shift_select(request): + page_number = request.GET.get("page") + + if page_number == "all": + if request.user.has_perm("base.view_shiftrequest"): + employees = ShiftRequest.objects.all() + else: + employees = ShiftRequest.objects.filter( + employee_id__employee_user_id=request.user + ) | ShiftRequest.objects.filter( + employee_id__employee_work_info__reporting_manager_id__employee_user_id=request.user + ) + # employees = ShiftRequest.objects.all() + + employee_ids = [str(emp.id) for emp in employees] + total_count = employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context, safe=False) + + +@login_required +def shift_select_filter(request): + page_number = request.GET.get("page") + filtered = request.GET.get("filter") + filters = json.loads(filtered) if filtered else {} + + if page_number == "all": + employee_filter = ShiftRequestFilter( + filters, queryset=ShiftRequest.objects.all() + ) + + # Get the filtered queryset + filtered_employees = employee_filter.qs + + employee_ids = [str(emp.id) for emp in filtered_employees] + total_count = filtered_employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context) + + +@login_required +def work_type_select(request): + page_number = request.GET.get("page") + + if page_number == "all": + if request.user.has_perm("base.view_worktyperequest"): + employees = WorkTypeRequest.objects.all() + else: + employees = WorkTypeRequest.objects.filter( + employee_id__employee_user_id=request.user + ) | WorkTypeRequest.objects.filter( + employee_id__employee_work_info__reporting_manager_id__employee_user_id=request.user + ) + + employee_ids = [str(emp.id) for emp in employees] + total_count = employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context, safe=False) + + +@login_required +def work_type_select_filter(request): + page_number = request.GET.get("page") + filtered = request.GET.get("filter") + filters = json.loads(filtered) if filtered else {} + + if page_number == "all": + employee_filter = WorkTypeRequestFilter( + filters, queryset=WorkTypeRequest.objects.all() + ) + + # Get the filtered queryset + filtered_employees = employee_filter.qs + + employee_ids = [str(emp.id) for emp in filtered_employees] + total_count = filtered_employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context) + + +@login_required +def rotating_shift_select(request): + page_number = request.GET.get("page") + + if page_number == "all": + if request.user.has_perm("base.view_rotatingshiftassign"): + employees = RotatingShiftAssign.objects.filter(is_active=True) + else: + employees = RotatingShiftAssign.objects.filter( + employee_id__employee_work_info__reporting_manager_id__employee_user_id=request.user + ) + else: + employees = RotatingShiftAssign.objects.all() + + employee_ids = [str(emp.id) for emp in employees] + total_count = employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context, safe=False) + + +@login_required +def rotating_shift_select_filter(request): + page_number = request.GET.get("page") + filtered = request.GET.get("filter") + filters = json.loads(filtered) if filtered else {} + + if page_number == "all": + employee_filter = RotatingShiftAssignFilters( + filters, queryset=RotatingShiftAssign.objects.all() + ) + + # Get the filtered queryset + filtered_employees = employee_filter.qs + + employee_ids = [str(emp.id) for emp in filtered_employees] + total_count = filtered_employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context) + + +@login_required +def rotating_work_type_select(request): + page_number = request.GET.get("page") + + if page_number == "all": + if request.user.has_perm("base.view_rotatingworktypeassign"): + employees = RotatingWorkTypeAssign.objects.filter(is_active=True) + else: + employees = RotatingWorkTypeAssign.objects.filter( + employee_id__employee_work_info__reporting_manager_id__employee_user_id=request.user + ) + else: + employees = RotatingWorkTypeAssign.objects.all() + + employee_ids = [str(emp.id) for emp in employees] + total_count = employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context, safe=False) + + +@login_required +def rotating_work_type_select_filter(request): + page_number = request.GET.get("page") + filtered = request.GET.get("filter") + filters = json.loads(filtered) if filtered else {} + + if page_number == "all": + employee_filter = RotatingWorkTypeAssignFilter( + filters, queryset=RotatingWorkTypeAssign.objects.all() + ) + + # Get the filtered queryset + filtered_employees = employee_filter.qs + + employee_ids = [str(emp.id) for emp in filtered_employees] + total_count = filtered_employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context) + + +@login_required +@permission_required("horilla_audit.view_audittag") +def tag_view(request): + """ + This method is used to show Audit tags + """ + audittags = AuditTag.objects.all() + return render( + request, + "base/tags/tags.html", + {"audittags": audittags}, + ) + + +@login_required +@permission_required("helpdesk.view_tag") +def helpdesk_tag_view(request): + """ + This method is used to show Help desk tags + """ + tags = Tags.objects.all() + return render( + request, + "base/tags/helpdesk_tags.html", + {"tags": tags}, + ) + + +@login_required +@hx_request_required +@permission_required("helpdesk.add_tag") +def tag_create(request): + """ + This method renders form and template to create Ticket type + """ + form = TagsForm() + if request.method == "POST": + form = TagsForm(request.POST) + if form.is_valid(): + form.save() + form = TagsForm() + messages.success(request, _("Tag has been created successfully!")) + return HttpResponse("") + return render( + request, + "base/tags/tags_form.html", + { + "form": form, + }, + ) + + +@login_required +@hx_request_required +@permission_required("helpdesk.change_tag") +def tag_update(request, tag_id): + """ + This method renders form and template to create Ticket type + """ + tag = Tags.objects.get(id=tag_id) + form = TagsForm(instance=tag) + if request.method == "POST": + form = TagsForm(request.POST, instance=tag) + if form.is_valid(): + form.save() + form = TagsForm() + messages.success(request, _("Tag has been updated successfully!")) + return HttpResponse("") + return render( + request, + "base/tags/tags_form.html", + {"form": form, "tag_id": tag_id}, + ) + + +@login_required +@hx_request_required +@permission_required("horilla_audit.add_audittag") +def audit_tag_create(request): + """ + This method renders form and template to create Ticket type + """ + form = AuditTagForm() + if request.method == "POST": + form = AuditTagForm(request.POST) + if form.is_valid(): + form.save() + form = AuditTagForm() + messages.success(request, _("Tag has been created successfully!")) + return HttpResponse("") + return render( + request, + "base/audit_tag/audit_tag_form.html", + { + "form": form, + }, + ) + + +@login_required +@hx_request_required +@permission_required("horilla_audit.change_audittag") +def audit_tag_update(request, tag_id): + """ + This method renders form and template to create Ticket type + """ + tag = AuditTag.objects.get(id=tag_id) + form = AuditTagForm(instance=tag) + if request.method == "POST": + form = AuditTagForm(request.POST, instance=tag) + if form.is_valid(): + form.save() + form = AuditTagForm() + messages.success(request, _("Tag has been updated successfully!")) + return HttpResponse("") + return render( + request, + "base/audit_tag/audit_tag_form.html", + {"form": form, "tag_id": tag_id}, + ) + + +@login_required +@permission_required("base.view_multipleapprovalcondition") +def multiple_approval_condition(request): + form = MultipleApproveConditionForm() + selected_company = request.session.get("selected_company") + if selected_company != "all": + conditions = MultipleApprovalCondition.objects.filter( + company_id=selected_company + ).order_by("department")[::-1] + else: + conditions = MultipleApprovalCondition.objects.all().order_by("department")[ + ::-1 + ] + create = True + return render( + request, + "multi_approval_condition/condition.html", + {"form": form, "conditions": conditions, "create": create}, + ) + + +@login_required +@hx_request_required +@permission_required("base.view_multipleapprovalcondition") +def hx_multiple_approval_condition(request): + selected_company = request.session.get("selected_company") + if selected_company != "all": + conditions = MultipleApprovalCondition.objects.filter( + company_id=selected_company + ).order_by("department")[::-1] + else: + conditions = MultipleApprovalCondition.objects.all().order_by("department")[ + ::-1 + ] + return render( + request, + "multi_approval_condition/condition_table.html", + {"conditions": conditions}, + ) + + +@login_required +@hx_request_required +@permission_required("base.add_multipleapprovalcondition") +def get_condition_value_fields(request): + operator = request.GET.get("condition_operator") + form = MultipleApproveConditionForm() + is_range = True if operator and operator == "range" else False + context = {"form": form, "range": is_range} + field_html = render_to_string( + "multi_approval_condition/condition_value_fields.html", context + ) + return HttpResponse(field_html) + + +@login_required +@hx_request_required +@permission_required("base.add_multipleapprovalcondition") +def add_more_approval_managers(request): + currnet_hx_target = request.META.get("HTTP_HX_TARGET") + hx_target_split = currnet_hx_target.split("_") + next_hx_target = "_".join([hx_target_split[0], str(int(hx_target_split[-1]) + 1)]) + + form = MultipleApproveConditionForm() + managers_count = request.GET.get("managers_count") + context = { + "next_hx_target": next_hx_target, + "currnet_hx_target": currnet_hx_target, + } + if managers_count: + managers_count = int(managers_count) + 1 + field_name = f"multi_approval_manager_{managers_count}" + choices = [("reporting_manager_id", _("Reporting Manager"))] + [ + (employee.pk, str(employee)) for employee in Employee.objects.all() + ] + form.fields[field_name] = forms.ChoiceField( + choices=choices, + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2 mb-3", + "name": field_name, + "id": f"id_{field_name}", + } + ), + required=False, + ) + context["managers_count"] = managers_count + context["field_html"] = form[field_name].as_widget() + else: + form.fields["multi_approval_manager"].widget.attrs.update( + { + "name": f"multi_approval_manager_{str(int(hx_target_split[-1]) + 1)}", + "id": f"id_multi_approval_manager_{str(int(hx_target_split[-1]) + 1)}", + } + ) + context["form"] = form + + field_html = render_to_string( + "multi_approval_condition/add_more_approval_manager.html", context + ) + + return HttpResponse(field_html) + + +@login_required +@hx_request_required +@permission_required("base.add_multipleapprovalcondition") +def remove_approval_manager(request): + return HttpResponse() + + +@login_required +@hx_request_required +@permission_required("base.add_multipleapprovalcondition") +def multiple_level_approval_create(request): + form = MultipleApproveConditionForm() + create = True + if request.method == "POST": + form = MultipleApproveConditionForm(request.POST) + dept_id = request.POST.get("department") + condition_field = request.POST.get("condition_field") + condition_operator = request.POST.get("condition_operator") + condition_value = request.POST.get("condition_value") + condition_start_value = request.POST.get("condition_start_value") + condition_end_value = request.POST.get("condition_end_value") + company_id = request.POST.get("company_id") + condition_approval_managers = request.POST.getlist("multi_approval_manager") + company = Company.objects.get(id=company_id) + department = Department.objects.get(id=dept_id) + instance = MultipleApprovalCondition() + if form.is_valid(): + instance.department = department + instance.condition_field = condition_field + instance.condition_operator = condition_operator + instance.company_id = company + if condition_operator != "range": + instance.condition_value = condition_value + else: + instance.condition_start_value = condition_start_value + instance.condition_end_value = condition_end_value + + instance.save() + sequence = 0 + for emp_id in condition_approval_managers: + sequence += 1 + reporting_manager = None + try: + employee_id = int(emp_id) + except: + employee_id = None + reporting_manager = emp_id + MultipleApprovalManagers.objects.create( + condition_id=instance, + sequence=sequence, + employee_id=employee_id, + reporting_manager=reporting_manager, + ) + form = MultipleApproveConditionForm() + messages.success( + request, _("Multiple approval condition created successfully") + ) + return render( + request, + "multi_approval_condition/condition_create_form.html", + {"form": form, "create": create}, + ) + + +def edit_approval_managers(form, managers): + for i, manager in enumerate(managers): + if i == 0: + form.initial["multi_approval_manager"] = manager.employee_id + else: + field_name = f"multi_approval_manager_{i}" + choices = [("reporting_manager_id", _("Reporting Manager"))] + [ + (employee.pk, str(employee)) for employee in Employee.objects.all() + ] + form.fields[field_name] = forms.ChoiceField( + choices=choices, + label=_("Approval Manager {}").format(i), + widget=forms.Select(attrs={"class": "oh-select oh-select-2 mb-3"}), + required=False, + ) + form.initial[field_name] = manager.employee_id + return form + + +@login_required +@hx_request_required +@permission_required("base.change_multipleapprovalcondition") +def multiple_level_approval_edit(request, condition_id): + create = False + condition = MultipleApprovalCondition.objects.get(id=condition_id) + managers = MultipleApprovalManagers.objects.filter(condition_id=condition).order_by( + "sequence" + ) + form = MultipleApproveConditionForm(instance=condition) + edit_approval_managers(form, managers) + if request.method == "POST": + form = MultipleApproveConditionForm(request.POST, instance=condition) + if form.is_valid(): + instance = form.save() + messages.success( + request, _("Multiple approval condition updated successfully") + ) + sequence = 0 + MultipleApprovalManagers.objects.filter(condition_id=condition).delete() + for key, value in request.POST.items(): + if key.startswith("multi_approval_manager"): + sequence += 1 + reporting_manager = None + try: + employee_id = int(value) + except: + employee_id = None + reporting_manager = value + MultipleApprovalManagers.objects.create( + condition_id=instance, + sequence=sequence, + employee_id=employee_id, + reporting_manager=reporting_manager, + ) + selected_company = request.session.get("selected_company") + if selected_company != "all": + conditions = MultipleApprovalCondition.objects.filter( + company_id=selected_company + ).order_by("department")[::-1] + else: + conditions = MultipleApprovalCondition.objects.all().order_by("department")[ + ::-1 + ] + + return render( + request, + "multi_approval_condition/condition_edit_form.html", + { + "form": form, + "conditions": conditions, + "create": create, + "condition": condition, + "managers_count": len(managers), + }, + ) + + +@login_required +@permission_required("base.delete_multipleapprovalcondition") +def multiple_level_approval_delete(request, condition_id): + condition = MultipleApprovalCondition.objects.get(id=condition_id) + condition.delete() + messages.success(request, _("Multiple approval condition deleted successfully")) + return redirect(hx_multiple_approval_condition) + + +@login_required +@hx_request_required +def create_shiftrequest_comment(request, shift_id): + """ + This method renders form and template to create shift request comments + """ + shift = ShiftRequest.find(shift_id) + emp = request.user.employee_get + form = ShiftRequestCommentForm( + initial={"employee_id": emp.id, "request_id": shift_id} + ) + + if request.method == "POST": + form = ShiftRequestCommentForm(request.POST) + if form.is_valid(): + form.instance.employee_id = emp + form.instance.request_id = shift + form.save() + comments = ShiftRequestComment.objects.filter(request_id=shift_id).order_by( + "-created_at" + ) + no_comments = False + if not comments.exists(): + no_comments = True + form = ShiftRequestCommentForm( + initial={"employee_id": emp.id, "request_id": shift_id} + ) + messages.success(request, _("Comment added successfully!")) + work_info = EmployeeWorkInformation.objects.filter( + employee_id=shift.employee_id + ) + if work_info.exists(): + if ( + shift.employee_id.employee_work_info.reporting_manager_id + is not None + ): + if request.user.employee_get.id == shift.employee_id.id: + rec = ( + shift.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ) + notify.send( + request.user.employee_get, + recipient=rec, + verb=f"{shift.employee_id}'s shift request has received a comment.", + verb_ar=f"تلقت طلب تحويل {shift.employee_id} تعليقًا.", + verb_de=f"{shift.employee_id}s Schichtantrag hat einen Kommentar erhalten.", + verb_es=f"La solicitud de turno de {shift.employee_id} ha recibido un comentario.", + verb_fr=f"La demande de changement de poste de {shift.employee_id} a reçu un commentaire.", + redirect=reverse("shift-request-view") + f"?id={shift.id}", + icon="chatbox-ellipses", + ) + elif ( + request.user.employee_get.id + == shift.employee_id.employee_work_info.reporting_manager_id.id + ): + rec = shift.employee_id.employee_user_id + notify.send( + request.user.employee_get, + recipient=rec, + verb="Your shift request has received a comment.", + verb_ar="تلقت طلبك للتحول تعليقًا.", + verb_de="Ihr Schichtantrag hat einen Kommentar erhalten.", + verb_es="Tu solicitud de turno ha recibido un comentario.", + verb_fr="Votre demande de changement de poste a reçu un commentaire.", + redirect=reverse("shift-request-view") + f"?id={shift.id}", + icon="chatbox-ellipses", + ) + else: + rec = [ + shift.employee_id.employee_user_id, + shift.employee_id.employee_work_info.reporting_manager_id.employee_user_id, + ] + notify.send( + request.user.employee_get, + recipient=rec, + verb=f"{shift.employee_id}'s shift request has received a comment.", + verb_ar=f"تلقت طلب تحويل {shift.employee_id} تعليقًا.", + verb_de=f"{shift.employee_id}s Schichtantrag hat einen Kommentar erhalten.", + verb_es=f"La solicitud de turno de {shift.employee_id} ha recibido un comentario.", + verb_fr=f"La demande de changement de poste de {shift.employee_id} a reçu un commentaire.", + redirect=reverse("shift-request-view") + f"?id={shift.id}", + icon="chatbox-ellipses", + ) + else: + rec = shift.employee_id.employee_user_id + notify.send( + request.user.employee_get, + recipient=rec, + verb="Your shift request has received a comment.", + verb_ar="تلقت طلبك للتحول تعليقًا.", + verb_de="Ihr Schichtantrag hat einen Kommentar erhalten.", + verb_es="Tu solicitud de turno ha recibido un comentario.", + verb_fr="Votre demande de changement de poste a reçu un commentaire.", + redirect=reverse("shift-request-view") + f"?id={shift.id}", + icon="chatbox-ellipses", + ) + return render( + request, + "shift_request/htmx/shift_comment.html", + { + "comments": comments, + "no_comments": no_comments, + "request_id": shift_id, + "shift_request": shift, + }, + ) + return render( + request, + "shift_request/htmx/shift_comment.html", + {"form": form, "request_id": shift_id, "shift_request": shift}, + ) + + +@login_required +@hx_request_required +def view_shift_comment(request, shift_id): + """ + This method is used to render all the notes of the employee + """ + shift_request = ShiftRequest.find(shift_id) + comments = ShiftRequestComment.objects.filter(request_id=shift_id).order_by( + "-created_at" + ) + no_comments = False + if not comments.exists(): + no_comments = True + if request.FILES: + files = request.FILES.getlist("files") + comment_id = request.GET["comment_id"] + comment = ShiftRequestComment.objects.get(id=comment_id) + attachments = [] + for file in files: + file_instance = BaserequestFile() + file_instance.file = file + file_instance.save() + attachments.append(file_instance) + comment.files.add(*attachments) + return render( + request, + "shift_request/htmx/shift_comment.html", + { + "comments": comments, + "no_comments": no_comments, + "request_id": shift_id, + "shift_request": shift_request, + }, + ) + + +@login_required +@hx_request_required +def delete_shift_comment_file(request): + """ + Used to delete attachment + """ + ids = request.GET.getlist("ids") + shift_id = request.GET["shift_id"] + comment_id = request.GET["comment_id"] + comment = ShiftRequestComment.find(comment_id) + script = "" + if ( + request.user.employee_get == comment.employee_id + or request.user.has_perm("base.delete_baserequestfile") + or is_reportingmanager(request) + ): + BaserequestFile.objects.filter(id__in=ids).delete() + messages.success(request, _("File deleted successfully")) + else: + messages.warning(request, _("You don't have permission")) + script = f"""""" + return HttpResponse(script) + + +@login_required +@hx_request_required +def view_work_type_comment(request, work_type_id): + """ + This method is used to render all the notes of the employee + """ + work_type_request = WorkTypeRequest.find(work_type_id) + comments = WorkTypeRequestComment.objects.filter(request_id=work_type_id).order_by( + "-created_at" + ) + no_comments = False + if not comments.exists(): + no_comments = True + if request.FILES: + files = request.FILES.getlist("files") + comment_id = request.GET["comment_id"] + comment = WorkTypeRequestComment.objects.get(id=comment_id) + attachments = [] + for file in files: + file_instance = BaserequestFile() + file_instance.file = file + file_instance.save() + attachments.append(file_instance) + comment.files.add(*attachments) + return render( + request, + "work_type_request/htmx/work_type_comment.html", + { + "comments": comments, + "no_comments": no_comments, + "request_id": work_type_id, + "work_type_request": work_type_request, + }, + ) + + +@login_required +@hx_request_required +def delete_work_type_comment_file(request): + """ + Used to delete attachment + """ + ids = request.GET.getlist("ids") + request_id = request.GET["request_id"] + comment_id = request.GET["comment_id"] + comment = WorkTypeRequestComment.find(comment_id) + script = "" + if ( + request.user.employee_get == comment.employee_id + or request.user.has_perm("base.delete_baserequestfile") + or is_reportingmanager(request) + ): + BaserequestFile.objects.filter(id__in=ids).delete() + messages.success(request, _("File deleted successfully")) + else: + messages.warning(request, _("You don't have permission")) + script = f"""""" + return HttpResponse(script) + + +@login_required +@hx_request_required +def delete_shiftrequest_comment(request, comment_id): + """ + This method is used to delete shift request comments + """ + comment = ShiftRequestComment.find(comment_id) + request_id = comment.request_id.id + script = "" + if ( + request.user.employee_get == comment.employee_id + or request.user.has_perm("base.delete_baserequestfile") + or is_reportingmanager(request) + ): + comment.delete() + messages.success(request, _("Comment deleted successfully!")) + else: + messages.warning(request, _("You don't have permission")) + script = f"""""" + return HttpResponse(script) + + +@login_required +@hx_request_required +def create_worktyperequest_comment(request, worktype_id): + """ + This method renders form and template to create Work type request comments + """ + work_type = WorkTypeRequest.objects.filter(id=worktype_id).first() + emp = request.user.employee_get + form = WorkTypeRequestCommentForm( + initial={"employee_id": emp.id, "request_id": worktype_id} + ) + + if request.method == "POST": + form = WorkTypeRequestCommentForm(request.POST) + if form.is_valid(): + form.instance.employee_id = emp + form.instance.request_id = work_type + form.save() + comments = WorkTypeRequestComment.objects.filter( + request_id=worktype_id + ).order_by("-created_at") + no_comments = False + if not comments.exists(): + no_comments = True + form = WorkTypeRequestCommentForm( + initial={"employee_id": emp.id, "request_id": worktype_id} + ) + messages.success(request, _("Comment added successfully!")) + + work_info = EmployeeWorkInformation.objects.filter( + employee_id=work_type.employee_id + ) + if work_info.exists(): + if ( + work_type.employee_id.employee_work_info.reporting_manager_id + is not None + ): + if request.user.employee_get.id == work_type.employee_id.id: + rec = ( + work_type.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ) + notify.send( + request.user.employee_get, + recipient=rec, + verb=f"{work_type.employee_id}'s work type request has received a comment.", + verb_ar=f"تلقت طلب نوع العمل {work_type.employee_id} تعليقًا.", + verb_de=f"{work_type.employee_id}s Arbeitsart-Antrag hat einen Kommentar erhalten.", + verb_es=f"La solicitud de tipo de trabajo de {work_type.employee_id} ha recibido un comentario.", + verb_fr=f"La demande de type de travail de {work_type.employee_id} a reçu un commentaire.", + redirect=reverse("work-type-request-view") + + f"?id={work_type.id}", + icon="chatbox-ellipses", + ) + elif ( + request.user.employee_get.id + == work_type.employee_id.employee_work_info.reporting_manager_id.id + ): + rec = work_type.employee_id.employee_user_id + notify.send( + request.user.employee_get, + recipient=rec, + verb="Your work type request has received a comment.", + verb_ar="تلقى طلب نوع العمل الخاص بك تعليقًا.", + verb_de="Ihr Arbeitsart-Antrag hat einen Kommentar erhalten.", + verb_es="Tu solicitud de tipo de trabajo ha recibido un comentario.", + verb_fr="Votre demande de type de travail a reçu un commentaire.", + redirect=reverse("work-type-request-view") + + f"?id={work_type.id}", + icon="chatbox-ellipses", + ) + else: + rec = [ + work_type.employee_id.employee_user_id, + work_type.employee_id.employee_work_info.reporting_manager_id.employee_user_id, + ] + notify.send( + request.user.employee_get, + recipient=rec, + verb=f"{work_type.employee_id}'s work type request has received a comment.", + verb_ar=f"تلقت طلب نوع العمل {work_type.employee_id} تعليقًا.", + verb_de=f"{work_type.employee_id}s Arbeitsart-Antrag hat einen Kommentar erhalten.", + verb_es=f"La solicitud de tipo de trabajo de {work_type.employee_id} ha recibido un comentario.", + verb_fr=f"La demande de type de travail de {work_type.employee_id} a reçu un commentaire.", + redirect=reverse("work-type-request-view") + + f"?id={work_type.id}", + icon="chatbox-ellipses", + ) + else: + rec = work_type.employee_id.employee_user_id + notify.send( + request.user.employee_get, + recipient=rec, + verb="Your work type request has received a comment.", + verb_ar="تلقى طلب نوع العمل الخاص بك تعليقًا.", + verb_de="Ihr Arbeitsart-Antrag hat einen Kommentar erhalten.", + verb_es="Tu solicitud de tipo de trabajo ha recibido un comentario.", + verb_fr="Votre demande de type de travail a reçu un commentaire.", + redirect=reverse("work-type-request-view") + + f"?id={work_type.id}", + icon="chatbox-ellipses", + ) + return render( + request, + "work_type_request/htmx/work_type_comment.html", + { + "comments": comments, + "no_comments": no_comments, + "request_id": worktype_id, + "work_type_request": work_type, + }, + ) + return render( + request, + "work_type_request/htmx/work_type_comment.html", + {"form": form, "request_id": worktype_id, "work_type_request": work_type}, + ) + + +@login_required +@hx_request_required +def delete_worktyperequest_comment(request, comment_id): + """ + This method is used to delete Work type request comments + """ + script = "" + comment = WorkTypeRequestComment.find(comment_id) + request_id = comment.request_id.id + if ( + request.user.employee_get == comment.employee_id + or request.user.has_perm("base.delete_baserequestfile") + or is_reportingmanager(request) + ): + comment.delete() + messages.success(request, _("Comment deleted successfully!")) + else: + messages.warning(request, _("You don't have permission")) + script = f"""""" + return HttpResponse(script) + + +@login_required +def pagination_settings_view(request): + if DynamicPagination.objects.filter(user_id=request.user).exists(): + pagination = DynamicPagination.objects.filter(user_id=request.user).first() + pagination_form = DynamicPaginationForm(instance=pagination) + if request.method == "POST": + pagination_form = DynamicPaginationForm(request.POST, instance=pagination) + if pagination_form.is_valid(): + pagination_form.save() + messages.success(request, _("Default pagination updated.")) + else: + pagination_form = DynamicPaginationForm() + if request.method == "POST": + pagination_form = DynamicPaginationForm( + request.POST, + ) + if pagination_form.is_valid(): + pagination_form.save() + messages.success(request, _("Default pagination updated.")) + if request.META.get("HTTP_HX_REQUEST"): + return HttpResponse() + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@permission_required("employee.view_actiontype") +def action_type_view(request): + """ + This method is used to show Action Type + """ + action_types = Actiontype.objects.all() + return render( + request, "base/action_type/action_type.html", {"action_types": action_types} + ) + + +@login_required +@hx_request_required +@permission_required("employee.add_actiontype") +def action_type_create(request): + """ + This method renders form and template to create Action Type + """ + form = ActiontypeForm() + previous_data = request.GET.urlencode() + dynamic = request.GET.get("dynamic") + if request.method == "POST": + form = ActiontypeForm(request.POST) + if form.is_valid(): + form.save() + form = ActiontypeForm() + messages.success(request, _("Action has been created successfully!")) + if dynamic != None: + url = reverse("create-actions") + instance = Actiontype.objects.all().order_by("-id").first() + mutable_get = request.GET.copy() + mutable_get["action"] = str(instance.id) + return redirect(f"{url}?{mutable_get.urlencode()}") + + return render( + request, + "base/action_type/action_type_form.html", + { + "form": form, + "pd": previous_data, + }, + ) + + +@login_required +@hx_request_required +@permission_required("employee.change_actiontype") +def action_type_update(request, act_id): + """ + This method renders form and template to update Action type + """ + action = Actiontype.objects.get(id=act_id) + form = ActiontypeForm(instance=action) + + if action.action_type == "warning": + if ( + AccountBlockUnblock.objects.first() + and AccountBlockUnblock.objects.first().is_enabled + ): + form.fields["block_option"].widget = forms.HiddenInput() + + if request.method == "POST": + form = ActiontypeForm(request.POST, instance=action) + if form.is_valid(): + act_type = form.cleaned_data["action_type"] + if act_type == "warning": + form.instance.block_option = False + form.save() + form = ActiontypeForm() + messages.success(request, _("Action has been updated successfully!")) + return render( + request, + "base/action_type/action_type_form.html", + {"form": form, "act_id": act_id}, + ) + + +@login_required +@hx_request_required +@permission_required("employee.delete_actiontype") +def action_type_delete(request, act_id): + """ + This method is used to delete the action type. + """ + if DisciplinaryAction.objects.filter(action=act_id).exists(): + + messages.error( + request, + _( + "This action type is in use in disciplinary actions and cannot be deleted." + ), + ) + return HttpResponse("") + + else: + Actiontype.objects.filter(id=act_id).delete() + messages.success(request, _("Action has been deleted successfully!")) + return HttpResponse() + + +@login_required +def driver_viewed_status(request): + """ + This method is used to update driver viewed status + """ + form = DriverForm(request.GET) + if form.is_valid(): + form.save() + return HttpResponse("") + + +@login_required +def dashboard_components_toggle(request): + """ + This function is used to create personalized dashboard charts for employees + """ + employee_charts = DashboardEmployeeCharts.objects.get_or_create( + employee=request.user.employee_get + )[0] + charts = employee_charts.charts or [] + chart_id = request.GET.get("chart_id") + if chart_id and chart_id not in charts: + charts.append(chart_id) + employee_charts.charts = charts + employee_charts.save() + return HttpResponse("") + + +def check_chart_permission(request, charts): + """ + This function is used to check the permissions for the charts + Args: + charts: dashboard charts + """ + from base.templatetags.basefilters import is_reportingmanager + + if apps.is_installed("recruitment"): + from recruitment.templatetags.recruitmentfilters import is_stagemanager + + need_stage_manager = [ + "hired_candidates", + "onboarding_candidates", + "recruitment_analytics", + ] + chart_apps = { + "offline_employees": "attendance", + "online_employees": "attendance", + "overall_leave_chart": "leave", + "hired_candidates": "recruitment", + "onboarding_candidates": "onboarding", + "recruitment_analytics": "recruitment", + "attendance_analytic": "attendance", + "hours_chart": "attendance", + "objective_status": "pms", + "key_result_status": "pms", + "feedback_status": "pms", + "shift_request_approve": "base", + "work_type_request_approve": "base", + "overtime_approve": "attendance", + "attendance_validate": "attendance", + "leave_request_approve": "leave", + "leave_allocation_approve": "leave", + "asset_request_approve": "asset", + "employees_chart": "employee", + "gender_chart": "employee", + "department_chart": "base", + } + permissions = { + "offline_employees": "employee.view_employee", + "online_employees": "employee.view_employee", + "overall_leave_chart": "leave.view_leaverequest", + "hired_candidates": "recruitment.view_candidate", + "onboarding_candidates": "recruitment.view_candidate", + "recruitment_analytics": "recruitment.view_recruitment", + "attendance_analytic": "attendance.view_attendance", + "hours_chart": "attendance.view_attendance", + "objective_status": "pms.view_employeeobjective", + "key_result_status": "pms.view_employeekeyresult", + "feedback_status": "pms.view_feedback", + "shift_request_approve": "base.change_shiftrequest", + "work_type_request_approve": "base.change_worktyperequest", + "overtime_approve": "attendance.change_attendance", + "attendance_validate": "attendance.change_attendance", + "leave_request_approve": "leave.change_leaverequest", + "leave_allocation_approve": "leave.change_leaveallocationrequest", + "asset_request_approve": "asset.change_assetrequest", + } + chart_list = [] + need_reporting_manager = [ + "offline_employees", + "online_employees", + "attendance_analytic", + "hours_chart", + "objective_status", + "key_result_status", + "feedback_status", + "shift_request_approve", + "work_type_request_approve", + "overtime_approve", + "attendance_validate", + "leave_request_approve", + "leave_allocation_approve", + "asset_request_approve", + ] + for chart in charts: + if apps.is_installed(chart_apps.get(chart[0])): + if ( + chart[0] in permissions.keys() + or chart[0] in need_reporting_manager + or (apps.is_installed("recruitment") and chart[0] in need_stage_manager) + ): + if request.user.has_perm(permissions[chart[0]]): + chart_list.append(chart) + elif chart[0] in need_reporting_manager: + if is_reportingmanager(request.user): + chart_list.append(chart) + elif ( + apps.is_installed("recruitment") and chart[0] in need_stage_manager + ): + if is_stagemanager(request.user): + chart_list.append(chart) + else: + chart_list.append(chart) + + return chart_list + + +@login_required +def employee_chart_show(request): + """ + This function is used to choose which chart to show in the dashboard + """ + employee_charts = DashboardEmployeeCharts.objects.get_or_create( + employee=request.user.employee_get + )[0] + charts = [ + ("offline_employees", _("Offline Employees")), + ("online_employees", _("Online Employees")), + ("overall_leave_chart", _("Overall Leave Chart")), + ("hired_candidates", _("Hired Candidates")), + ("onboarding_candidates", _("Onboarding Candidates")), + ("recruitment_analytics", _("Recruitment Analytics")), + ("attendance_analytic", _("Attendance analytics")), + ("hours_chart", _("Hours Chart")), + ("employees_chart", _("Employees Chart")), + ("department_chart", _("Department Chart")), + ("gender_chart", _("Gender Chart")), + ("objective_status", _("Objective Status")), + ("key_result_status", _("Key Result Status")), + ("feedback_status", _("Feedback Status")), + ("shift_request_approve", _("Shift Request to Approve")), + ("work_type_request_approve", _("Work Type Request to Approve")), + ("overtime_approve", _("Overtime to Approve")), + ("attendance_validate", _("Attendance to Validate")), + ("leave_request_approve", _("Leave Request to Approve")), + ("leave_allocation_approve", _("Leave Allocation to Approve")), + ("feedback_answer", _("Feedbacks to Answer")), + ("asset_request_approve", _("Asset Request to Approve")), + ] + charts = check_chart_permission(request, charts) + + if request.method == "POST": + employee_charts.charts = [] + employee_charts.save() + data = request.POST + for chart in charts: + if chart[0] not in data.keys() and chart[0] not in employee_charts.charts: + employee_charts.charts.append(chart[0]) + elif chart[0] in data.keys() and chart[0] in employee_charts.charts: + employee_charts.charts.remove(chart[0]) + else: + pass + + employee_charts.save() + return HttpResponse("") + context = {"dashboard_charts": charts, "employee_chart": employee_charts.charts} + return render(request, "dashboard_chart_form.html", context) + + +@login_required +@permission_required("base.view_biometricattendance") +def enable_biometric_attendance_view(request): + biometric = BiometricAttendance.objects.first() + return render( + request, + "base/install_biometric_attendance.html", + {"biometric": biometric}, + ) + + +@login_required +@permission_required("base.add_biometricattendance") +def activate_biometric_attendance(request): + if request.method == "GET": + is_installed = request.GET.get("is_installed") + instance = BiometricAttendance.objects.first() + if not instance: + instance = BiometricAttendance.objects.create() + if is_installed == "true": + instance.is_installed = True + messages.success( + request, + _("The biometric attendance feature has been activated successfully."), + ) + else: + instance.is_installed = False + messages.info( + request, + _( + "The biometric attendance feature has been deactivated successfully." + ), + ) + instance.save() + return JsonResponse({"message": "Success"}) + + +@login_required +def get_horilla_installed_apps(request): + return JsonResponse({"installed_apps": APPS}) + + +def generate_error_report(error_list, error_data, file_name): + for item in error_list: + for key, value in error_data.items(): + if key in item: + value.append(item[key]) + else: + value.append(None) + + keys_to_remove = [ + key for key, value in error_data.items() if all(v is None for v in value) + ] + for key in keys_to_remove: + del error_data[key] + + data_frame = pd.DataFrame(error_data, columns=error_data.keys()) + styled_data_frame = data_frame.style.applymap( + lambda x: "text-align: center", subset=pd.IndexSlice[:, :] + ) + + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = f'attachment; filename="{file_name}"' + writer = pd.ExcelWriter(response, engine="xlsxwriter") + styled_data_frame.to_excel(writer, index=False, sheet_name="Sheet1") + + worksheet = writer.sheets["Sheet1"] + worksheet.set_column("A:Z", 30) + writer.close() + + def get_error_sheet(request): + remove_dynamic_url(path_info) + return response + + from base.urls import path, urlpatterns + + # Create a unique path for the error file download + path_info = f"error-sheet-{uuid.uuid4()}" + urlpatterns.append(path(path_info, get_error_sheet, name=path_info)) + DYNAMIC_URL_PATTERNS.append(path_info) + for key in error_data: + error_data[key] = [] + return path_info + + +@login_required +@hx_request_required +def get_upcoming_holidays(request): + """ + Retrieve and display a list of upcoming holidays for the current month and year. + """ + today = timezone.localdate() + current_year = today.year + holidays = Holidays.objects.filter( + start_date__year=current_year, start_date__gte=today + ) + colors = generate_colors(len(holidays)) + for i, holiday in enumerate(holidays): + holiday.background_color = colors[i] + return render(request, "holiday/upcoming_holidays.html", {"holidays": holidays}) + + +@login_required +@hx_request_required +@permission_required("base.add_holidays") +def holiday_creation(request): + """ + function used to create holidays. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return holiday creation form template + POST : return holiday view template + """ + + previous_data = request.GET.urlencode() + form = HolidayForm() + if request.method == "POST": + form = HolidayForm(request.POST) + if form.is_valid(): + form.save() + form = HolidayForm() + messages.success(request, _("New holiday created successfully..")) + if Holidays.objects.filter().count() == 1: + return HttpResponse("") + return render( + request, "holiday/holiday_form.html", {"form": form, "pd": previous_data} + ) + + +def holidays_excel_template(request): + try: + columns = [ + "Holiday Name", + "Start Date", + "End Date", + "Recurring", + ] + data_frame = pd.DataFrame(columns=columns) + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = ( + 'attachment; filename="assign_leave_type_excel.xlsx"' + ) + data_frame.to_excel(response, index=False) + return response + except Exception as exception: + return HttpResponse(exception) + + +def csv_holiday_import(file): + """ + Imports holiday data from a CSV file. + + This function reads a CSV file containing holiday information, validates the data, + and saves valid holiday records to the database using bulk creation for efficiency. + + The expected format for the CSV file is: + - "Holiday Name": Name of the holiday (string) + - "Start Date": Start date of the holiday (date string in a recognized format) + - "End Date": End date of the holiday (date string in a recognized format) + - "Recurring": Indicates whether the holiday recurs ("yes" or "no") + """ + holiday_list, error_list = [], [] + file_name = FILE_STORAGE.save("holiday_import.csv", ContentFile(file.read())) + holiday_file = FILE_STORAGE.path(file_name) + + with open(holiday_file, errors="ignore") as csv_file: + save = True + reader = csv.reader(csv_file) + next(reader) + + for total_rows, row in enumerate(reader, start=1): + try: + name, start_date, end_date, recurring = row + holiday_dict = { + "Holiday Name": name, + "Start Date": start_date, + "End Date": end_date, + "Recurring": recurring, + } + + try: + start_date = format_date(start_date) + except: + save = False + holiday_dict["Start Date Error"] = _("Invalid start date format.") + error_list.append(holiday_dict) + + try: + end_date = format_date(end_date) + except: + save = False + holiday_dict["End Date Error"] = _("Invalid end date format.") + error_list.append(holiday_dict) + + if recurring.lower() not in ["yes", "no"]: + save = False + holiday_dict["Recurring Field Error"] = _( + "Recurring must be yes or no." + ) + error_list.append(holiday_dict) + + if save: + holiday_list.append( + Holidays( + name=name, + start_date=start_date, + end_date=end_date, + recurring=recurring.lower() == "yes", + ) + ) + + except Exception as e: + holiday_dict["Other Errors"] = str(e) + error_list.append(holiday_dict) + + if holiday_list: + Holidays.objects.bulk_create(holiday_list) + + if os.path.exists(holiday_file): + os.remove(holiday_file) + + return (error_list, total_rows) + + +def excel_holiday_import(file): + """ + Imports holiday data from an Excel file. + + This function reads an Excel file containing holiday information, validates the data, + and saves valid holiday records to the database using bulk creation for efficiency + + The expected format for the Excel file is: + - "Holiday Name": Name of the holiday (string) + - "Start Date": Start date of the holiday (date string in a recognized format) + - "End Date": End date of the holiday (date string in a recognized format) + - "Recurring": Indicates whether the holiday recurs ("yes" or "no") + + """ + error_list = [] + valid_holidays = [] + data_frame = pd.read_excel(file) + holiday_dicts = data_frame.to_dict("records") + + for holiday in holiday_dicts: + save = True + try: + name = holiday["Holiday Name"] + + try: + start_date = pd.to_datetime(holiday["Start Date"]).date() + except Exception: + save = False + holiday["Start Date Error"] = _("Invalid start date format {}").format( + holiday["Start Date"] + ) + + try: + end_date = pd.to_datetime(holiday["End Date"]).date() + except Exception: + save = False + holiday["End Date Error"] = _("Invalid end date format {}").format( + holiday["End Date"] + ) + + recurring_str = holiday.get("Recurring", "").lower() + if recurring_str in ["yes", "no"]: + recurring = recurring_str == "yes" + else: + save = False + holiday["Recurring Field Error"] = _( + "Recurring must be {} or {}" + ).format("yes", "no") + + if save: + holiday_instance = Holidays( + name=name, + start_date=start_date, + end_date=end_date, + recurring=recurring, + ) + valid_holidays.append(holiday_instance) + else: + error_list.append(holiday) + + except Exception as e: + holiday["Other errors"] = str(e) + error_list.append(holiday) + + if valid_holidays: + Holidays.objects.bulk_create(valid_holidays) + + return error_list, len(holiday_dicts) + + +@login_required +@permission_required("base.add_holiday") +def holidays_info_import(request): + result = None + file_name = "HolidaysImportError.xlsx" + path_info = None + error_data = { + "Holiday Name": [], + "Start Date": [], + "End Date": [], + "Recurring": [], + "Start Date Error": [], + "End Date Error": [], + "Recurring Field Error": [], + "Other Errors": [], + } + + if request.method == "POST": + file = request.FILES.get("holidays_import") + if file: + content_type = file.content_type + if content_type == "text/csv": + error_list, total_count = csv_holiday_import(file) + if error_list: + path_info = generate_error_report(error_list, error_data, file_name) + elif ( + content_type + == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ): + error_list, total_count = excel_holiday_import(file) + if error_list: + path_info = generate_error_report(error_list, error_data, file_name) + else: + messages.error( + request, _("The file you attempted to import is unsupported") + ) + return HttpResponse("") + + created_holidays_count = total_count - len(error_list) + context = { + "created_count": created_holidays_count, + "error_count": len(error_list), + "model": _("Holidays"), + "path_info": path_info, + } + result = render_to_string("import_popup.html", context) + + return HttpResponse(result) + + +@login_required +def holiday_info_export(request): + if request.META.get("HTTP_HX_REQUEST"): + export_filter = HolidayFilter() + export_column = HolidaysColumnExportForm() + content = { + "export_filter": export_filter, + "export_column": export_column, + } + return render( + request, "holiday/holiday_export_filter_form.html", context=content + ) + return export_data( + request=request, + model=Holidays, + filter_class=HolidayFilter, + form_class=HolidaysColumnExportForm, + file_name="Holidays_export", + ) + + +@login_required +def holiday_view(request): + """ + function used to view holidays. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return holiday view template + """ + queryset = Holidays.objects.all()[::-1] + previous_data = request.GET.urlencode() + page_number = request.GET.get("page") + page_obj = paginator_qry(queryset, page_number) + holiday_filter = HolidayFilter() + + return render( + request, + "holiday/holiday_view.html", + { + "holidays": page_obj, + "form": holiday_filter.form, + "pd": previous_data, + }, + ) + + +@login_required +@hx_request_required +def holiday_filter(request): + """ + function used to filter holidays. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return holiday view template + """ + queryset = Holidays.objects.all() + previous_data = request.GET.urlencode() + holiday_filter = HolidayFilter(request.GET, queryset).qs + if request.GET.get("sortby"): + holiday_filter = sortby(request, holiday_filter, "sortby") + page_number = request.GET.get("page") + page_obj = paginator_qry(holiday_filter[::-1], page_number) + data_dict = parse_qs(previous_data) + get_key_instances(Holidays, data_dict) + return render( + request, + "holiday/holiday.html", + {"holidays": page_obj, "pd": previous_data, "filter_dict": data_dict}, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_holidays") +def holiday_update(request, obj_id): + """ + function used to update holiday. + + Parameters: + request (HttpRequest): The HTTP request object. + id : holiday id + + Returns: + GET : return holiday update form template + POST : return holiday view template + """ + query_string = request.GET.urlencode() + if query_string.startswith("pd="): + previous_data = unquote(query_string[len("pd=") :]) + else: + previous_data = unquote(query_string) + holiday = Holidays.objects.get(id=obj_id) + form = HolidayForm(instance=holiday) + if request.method == "POST": + form = HolidayForm(request.POST, instance=holiday) + if form.is_valid(): + form.save() + messages.success(request, _("Holidays updated successfully..")) + return render( + request, + "holiday/holiday_update_form.html", + {"form": form, "id": obj_id, "pd": previous_data}, + ) + + +@login_required +@hx_request_required +@permission_required("base.delete_holidays") +def holiday_delete(request, obj_id): + """ + function used to delete holiday. + + Parameters: + request (HttpRequest): The HTTP request object. + id : holiday id + + Returns: + GET : return holiday view template + """ + query_string = request.GET.urlencode() + try: + Holidays.objects.get(id=obj_id).delete() + messages.success(request, _("Holidays deleted successfully..")) + except Holidays.DoesNotExist: + messages.error(request, _("Holidays not found.")) + except ProtectedError: + messages.error(request, _("Related entries exists")) + if not Holidays.objects.filter(): + return HttpResponse("") + return redirect(f"/holiday-filter?{query_string}") + + +@login_required +@require_http_methods(["POST"]) +@permission_required("base.delete_holiday") +def bulk_holiday_delete(request): + """ + Deletes multiple holidays based on IDs passed in the POST request. + """ + ids = request.POST.getlist("ids") + deleted_count = Holidays.objects.filter(id__in=ids).delete()[0] + messages.success( + request, _("{} Holidays have been successfully deleted.".format(deleted_count)) + ) + return redirect("holiday-filter") + + +@login_required +def holiday_select(request): + page_number = request.GET.get("page") + + if page_number == "all": + employees = Holidays.objects.all() + + employee_ids = [str(emp.id) for emp in employees] + total_count = employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context, safe=False) + + +@login_required +def holiday_select_filter(request): + page_number = request.GET.get("page") + filtered = request.GET.get("filter") + filters = json.loads(filtered) if filtered else {} + + if page_number == "all": + employee_filter = HolidayFilter(filters, queryset=Holidays.objects.all()) + + # Get the filtered queryset + filtered_employees = employee_filter.qs + + employee_ids = [str(emp.id) for emp in filtered_employees] + total_count = filtered_employees.count() + + context = {"employee_ids": employee_ids, "total_count": total_count} + + return JsonResponse(context) + + +@login_required +@hx_request_required +@permission_required("base.add_companyleaves") +def company_leave_creation(request): + """ + function used to create company leave. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return company leave creation form template + POST : return company leave view template + """ + form = CompanyLeaveForm() + if request.method == "POST": + form = CompanyLeaveForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("New company leave created successfully..")) + if CompanyLeaves.objects.filter().count() == 1: + return HttpResponse("") + return render( + request, "company_leave/company_leave_creation_form.html", {"form": form} + ) + + +@login_required +def company_leave_view(request): + """ + function used to view company leave. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return company leave view template + """ + queryset = CompanyLeaves.objects.all() + previous_data = request.GET.urlencode() + page_number = request.GET.get("page") + page_obj = paginator_qry(queryset, page_number) + company_leave_filter = CompanyLeaveFilter() + return render( + request, + "company_leave/company_leave_view.html", + { + "company_leaves": page_obj, + "weeks": WEEKS, + "week_days": WEEK_DAYS, + "form": company_leave_filter.form, + "pd": previous_data, + }, + ) + + +@login_required +@hx_request_required +def company_leave_filter(request): + """ + function used to filter company leave. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return company leave view template + """ + queryset = CompanyLeaves.objects.all() + previous_data = request.GET.urlencode() + page_number = request.GET.get("page") + company_leave_filter = CompanyLeaveFilter(request.GET, queryset).qs + page_obj = paginator_qry(company_leave_filter, page_number) + data_dict = parse_qs(previous_data) + get_key_instances(CompanyLeaves, data_dict) + + return render( + request, + "company_leave/company_leave.html", + { + "company_leaves": page_obj, + "weeks": WEEKS, + "week_days": WEEK_DAYS, + "pd": previous_data, + "filter_dict": data_dict, + }, + ) + + +@login_required +@hx_request_required +@permission_required("base.change_companyleaves") +def company_leave_update(request, id): + """ + function used to update company leave. + + Parameters: + request (HttpRequest): The HTTP request object. + id : company leave id + + Returns: + GET : return company leave update form template + POST : return company leave view template + """ + company_leave = CompanyLeaves.objects.get(id=id) + form = CompanyLeaveForm(instance=company_leave) + if request.method == "POST": + form = CompanyLeaveForm(request.POST, instance=company_leave) + if form.is_valid(): + form.save() + messages.success(request, _("Company leave updated successfully..")) + return render( + request, + "company_leave/company_leave_update_form.html", + {"form": form, "id": id}, + ) + + +@login_required +@hx_request_required +@permission_required("base.delete_companyleaves") +def company_leave_delete(request, id): + """ + function used to create company leave. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + GET : return company leave creation form template + POST : return company leave view template + """ + query_string = request.GET.urlencode() + try: + CompanyLeaves.objects.get(id=id).delete() + messages.success(request, _("Company leave deleted successfully..")) + except CompanyLeaves.DoesNotExist: + messages.error(request, _("Company leave not found.")) + except ProtectedError: + messages.error(request, _("Related entries exists")) + if not CompanyLeaves.objects.filter(): + return HttpResponse("") + return redirect(f"/company-leave-filter?{query_string}") + + +@login_required +@hx_request_required +def view_penalties(request): + """ + This method is used to filter or view the penalties + """ + records = PenaltyFilter(request.GET).qs + return render(request, "penalty/penalty_view.html", {"records": records}) + + +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from rest_framework_simplejwt.tokens import UntypedToken + + +def is_jwt_token_valid(auth_header): + if not auth_header or not auth_header.startswith("Bearer "): + return None # No token + + token = auth_header.split("Bearer ")[1].strip() + try: + UntypedToken(token) # Will raise if invalid + validated_token = JWTAuthentication().get_validated_token(token) + user = JWTAuthentication().get_user(validated_token) + return user + except (InvalidToken, TokenError): + return None + + +def protected_media(request, path): + public_pages = [ + "/login", + "/forgot-password", + "/change-username", + "/change-password", + "/employee-reset-password", + "/recruitment/candidate-survey", + "/recruitment/open-recruitments", + "/recruitment/candidate-self-status-tracking", + ] + exempted_folders = ["base/icon/"] + + media_path = os.path.join(settings.MEDIA_ROOT, path) + if not os.path.exists(media_path): + raise Http404("File not found") + + referer_path = urlparse(request.META.get("HTTP_REFERER", "")).path + + # Try Bearer token auth + jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) + + # Access control logic + if referer_path not in public_pages and not any( + path.startswith(f) for f in exempted_folders + ): + if not request.user.is_authenticated and not jwt_user: + messages.error( + request, + "You must be logged in or provide a valid token to access this file.", + ) + return redirect("login") + + return FileResponse(open(media_path, "rb"))