diff --git a/.dockerignore b/.dockerignore index 4b8641f80..7b74c129b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,4 @@ .gitignore *.md LICENSE -docker-compose.yaml \ No newline at end of file +docker-compose.yaml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f04225162..56d72a6fe 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,4 +5,4 @@ contact_links: about: Ask questions or discuss features here - name: Documentation url: https://horilla-opensource.github.io/horilla-docs/ - about: Check the official Horilla documentation \ No newline at end of file + about: Check the official Horilla documentation diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 1921e67bd..683882644 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -117,4 +117,4 @@ body: - label: I have searched for duplicate feature requests required: true - label: I have provided clear and concise information - required: true \ No newline at end of file + required: true diff --git a/asset/cbv/accessibility.py b/asset/cbv/accessibility.py new file mode 100644 index 000000000..b141027bf --- /dev/null +++ b/asset/cbv/accessibility.py @@ -0,0 +1,25 @@ + +from django.contrib.auth.context_processors import PermWrapper +from base.methods import check_manager +from employee.models import Employee + + +def asset_accessibility(request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs) -> bool: + """ + accessibility for asset tab + """ + employee = Employee.objects.get(id=instance.pk) + if ( + request.user.has_perm("asset.view_asset") or check_manager(request.user.employee_get, instance) + or request.user == employee.employee_user_id + ): + return True + return False + + +def create_asset_request_accessibility( + request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + if request.user.has_perm("asset.add_assetrequest"): + return True + return False \ No newline at end of file diff --git a/asset/cbv/asset_batch_no.py b/asset/cbv/asset_batch_no.py new file mode 100644 index 000000000..8b8fc6943 --- /dev/null +++ b/asset/cbv/asset_batch_no.py @@ -0,0 +1,189 @@ +""" +this page is handling the cbv methods for asset batch no +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from asset.filters import AssetBatchNoFilter +from asset.forms import AssetBatchForm +from asset.models import AssetLot +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetlot"), name="dispatch") +class AssetBatchNoView(TemplateView): + """ + for Asset batch no page + """ + + template_name = "cbv/asset_batch_no/asset_batch_no.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetlot"), name="dispatch") +class AssetBatchNoListView(HorillaListView): + """ + list view for batch number + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("asset-batch-list") + self.view_id = "AssetBatchList" + + model = AssetLot + filter_class = AssetBatchNoFilter + + columns = [ + (_("Batch Number"), "lot_number"), + (_("Description"), "lot_description"), + (_("Assets"),"assets_column") + ] + + + header_attrs = { + "action" : """ + style = "width:180px !important" + """ + } + + action_method = "actions" + + row_attrs = """ + hx-get='{asset_batch_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetlot"), name="dispatch") +class AssetBatchNoNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("asset-batch-list") + + if self.request.user.has_perm("asset.view_assetlot"): + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('asset-batch-number-creation')}" + """ + + nav_title = _("Asset Batch Number") + filter_instance = AssetBatchNoFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.add_assetlot"), name="dispatch") +class AssetBatchCreateFormView(HorillaFormView): + """ + form view for create batch number + """ + + form_class = AssetBatchForm + model = AssetLot + new_display_title = _("Create Batch Number") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Batch Number Update") + + return context + + def form_valid(self, form: AssetBatchForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Batch number updated successfully.") + else: + message = _("Batch number created successfully.") + form.save() + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.add_assetlot"), name="dispatch") +class DynamicCreateBatchNo(AssetBatchCreateFormView): + """ + view for dynamic batch create + """ + + is_dynamic_create_view = True + + +class AssetBatchDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + def get_context_data(self, **kwargs: Any): + """ + Return context data with the title set to the contract's name. + """ + + context = super().get_context_data(**kwargs) + lot_number = context["assetlot"].lot_number + context["title"] = "Asset Batch:" + lot_number + return context + + model = AssetLot + header = False + + cols = { + "assets_column":12, + "lot_description":12, + "lot_number":12 + } + body = { + (_("Assets"),"assets_column"), + (_("Description"), "lot_description"), + (_("Batch Number"), "lot_number"), + + } + + actions = [ + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--info w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + }, + { + "action": _("Delete"), + "icon": "trash-outline", + + "attrs": """ + class="oh-btn oh-btn--danger w-100" + hx-confirm="Do you want to delete this batch number?" + hx-post="{get_delete_url}?instance_ids={ordered_ids}" + hx-target="#AssetBatchList" + """ + }, + ] diff --git a/asset/cbv/asset_category.py b/asset/cbv/asset_category.py new file mode 100644 index 000000000..7aa58db28 --- /dev/null +++ b/asset/cbv/asset_category.py @@ -0,0 +1,309 @@ +""" +Asset category forms +""" + +from typing import Any +from django import forms +from django.contrib import messages +from django.http import HttpResponse +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from asset.cbv.asset_batch_no import DynamicCreateBatchNo +from asset.filters import AssetCategoryFilter, AssetFilter, CustomAssetFilter +from asset.forms import AssetCategoryForm, AssetForm, AssetReportForm +from asset.models import Asset, AssetCategory, AssetDocuments, AssetReport +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaNavView, +) +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetcategory"), name="dispatch") +class AssetCategoryFormView(HorillaFormView): + """ + form view for create asset category + """ + + form_class = AssetCategoryForm + model = AssetCategory + new_display_title = _("Asset Category Creation") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Asset Category Update") + + return context + + def form_valid(self, form: AssetCategoryForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Asset category updated successfully") + else: + message = _("Asset category created successfully") + form.save() + messages.success(self.request, _(message)) + return HttpResponse( + "" + ) + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.add_asset"), name="dispatch") +class AssetFormView(HorillaFormView): + """ + form view for create asset + """ + + form_class = AssetForm + model = Asset + new_display_title = _("Asset Creation") + dynamic_create_fields = [("asset_lot_number_id", DynamicCreateBatchNo)] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + asset_category_id = self.kwargs.get("asset_category_id") + self.form.fields["asset_category_id"].initial = asset_category_id + self.form.fields["asset_category_id"].widget = forms.HiddenInput() + if self.form.instance.pk: + self.form_class.verbose_name = _("Asset Update") + return context + + def form_valid(self, form: AssetForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Asset updated successfully") + else: + message = _("Asset created successfully") + form.save() + messages.success(self.request, _(message)) + return HttpResponse( + "" + ) + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetcategory"), name="dispatch") +class AssetCategoryDuplicateFormView(HorillaFormView): + """ + form view for create duplicate asset category + """ + + form_class = AssetCategoryForm + model = AssetCategory + new_display_title = _("Asset Category Duplicate") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = AssetCategory.objects.get(id=self.kwargs["obj_id"]) + form = self.form_class(instance=original_object) + 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 + context["form"] = form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_valid(self, form: AssetCategoryForm) -> HttpResponse: + if form.is_valid(): + message = _("Asset category created successfully") + form.save() + messages.success(self.request, _(message)) + return HttpResponse( + "" + ) + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_asset"), name="dispatch") +class AssetDuplicateFormView(HorillaFormView): + """ + form view for create duplicate for asset + """ + + form_class = AssetForm + model = Asset + new_display_title = _("Asset Duplicate") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = Asset.objects.get(id=self.kwargs["obj_id"]) + form = self.form_class(instance=original_object) + form.fields["asset_category_id"].widget = forms.HiddenInput() + 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 + context["form"] = form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_valid(self, form: AssetForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Asset updated successfully") + else: + message = _("Asset created successfully") + form.save() + messages.success(self.request, _(message)) + return HttpResponse( + "" + ) + return super().form_valid(form) + +class AssetReportFormView(HorillaFormView): + """ + form view for create button + """ + + form_class = AssetReportForm + model = AssetReport + new_display_title = _("Add Asset Report") + + def get_initial(self) -> dict: + initial = super().get_initial() + initial["asset_id"] = self.kwargs.get("asset_id") + return initial + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + asset_id = self.kwargs.get("asset_id") + asset = Asset.objects.filter(id=asset_id) + self.form.fields["asset_id"].queryset = asset + return context + + def form_valid(self, form: AssetReportForm) -> HttpResponse: + if form.is_valid(): + message = _("Asset report added successfully.") + asset = form.save() + uploaded_files = form.cleaned_data.get('files') + if uploaded_files: + for file in uploaded_files: + AssetDocuments.objects.create(asset_report=asset, file=file) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_asset"), name="dispatch") +class AssetCategoryNav(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("asset-category-view-search-filter") + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('asset-category-creation')}" + hx-target="#genericModalBody" + """ + # if self.request.user.has_perm( + # "attendance.add_attendanceovertime" + # ) or is_reportingmanager(self.request): + + # self.actions = [ + # { + # "action": _("Import"), + # "attrs": """ + # onclick=" + # reqAttendanceBulkApprove(); + # " + # style="cursor: pointer;" + # """, + # }, + # { + # "action": _("Export"), + # "attrs": """ + # onclick="reqAttendanceBulkReject();" + # style="color:red !important" + # """, + # }, + # ] + # else: + # self.actions = None + + nav_title = _("Asset Category") + filter_body_template = "cbv/asset_category/filter.html" + filter_instance = AssetFilter() + filter_form_context_name = "form" + search_swap_target = "#assetCategoryList" + + + + +class AssetCategoryDetailView(HorillaDetailedView): + """ + Detail view of the page + """ + + + + def get_context_data(self, **kwargs: Any): + """ + Return context data with the title set to the contract's name. + """ + + context = super().get_context_data(**kwargs) + asset_name = context["asset"].asset_name + context["title"] = asset_name + return context + + model = Asset + header = False + template_name = "cbv/asset_category/detail_view_action.html" + body = [ + (_("Tracking Id"), "asset_tracking_id"), + (_("Purchase Date"), "asset_purchase_date"), + (_("Cost"), "asset_purchase_cost"), + (_("Status"), "get_status_display"), + (_("Batch No"), "asset_lot_number_id__lot_number"), + (_("Category"), "asset_category_id"), + ] + + actions = [ + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--info w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + }, + { + "action": _("Delete"), + "icon": "trash-outline", + + "attrs": """ + class="oh-btn oh-btn--danger w-100" + hx-confirm="Do you want to delete this asset?" + hx-post="{get_delete_url}?instance_ids={ordered_ids}" + hx-target="#genericModalBody" + """ + }, + ] + + + diff --git a/asset/cbv/asset_history.py b/asset/cbv/asset_history.py new file mode 100644 index 000000000..fb606eef0 --- /dev/null +++ b/asset/cbv/asset_history.py @@ -0,0 +1,119 @@ +""" +this page is handling the cbv methods of asset history page +""" + +from typing import Any +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from asset.filters import AssetHistoryFilter +from asset.models import AssetAssignment +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + TemplateView, + HorillaListView, + HorillaNavView, +) +from horilla_views.cbv_methods import login_required, permission_required + + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetassignment"), name="dispatch") +class AssetHistoryView(TemplateView): + """ + for page view + """ + + template_name = "cbv/asset_history/asset_history_home.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetassignment"), name="dispatch") +class AssetHistorylistView(HorillaListView): + """ + list view + """ + + filter_class = AssetHistoryFilter + model = AssetAssignment + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("asset-history-list") + + columns = [ + (_("Asset"),"asset_id__asset_name", "get_avatar"), + (_("Employee"),"assigned_to_employee_id"), + (_("Assigned Date"),"assigned_date"), + (_("Returned Date"),"return_date"), + (_("Return Status"),"return_status") + ] + + records_per_page = 5 + + sortby_mapping = [ + ("Asset","asset_id__asset_name", "get_avatar"), + ("Employee","assigned_to_employee_id"), + ("Assigned Date","assigned_date"), + ("Returned Date","return_date"), + ] + + row_attrs = """ + hx-get='{asset_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetassignment"), name="dispatch") +class AssetHistoryNavView(HorillaNavView): + """ + navbar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("asset-history-list") + + nav_title = _("Asset History") + filter_body_template = "cbv/asset_history/asset_history_filter.html" + filter_form_context_name = "form" + filter_instance = AssetHistoryFilter() + search_swap_target = "#listContainer" + + group_by_fields = [ + ("asset_id__asset_name",_("Asset")), + ("assigned_to_employee_id",_("Employee")), + ("assigned_date",_("Assigned Date")), + ("return_date",_("Returned Date")), + + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("asset.view_assetassignment"), name="dispatch") +class AssetHistoryDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + model = AssetAssignment + title = _("Asset Details") + header = { + "title":"asset_id", + "subtitle":"asset_id__asset_category_id", + "avatar":"assigned_to_employee_id__get_avatar" + } + body = [ + (_("Allocated User"),"assigned_to_employee_id"), + (_("Returned Status"),"return_status"), + (_("Allocated Date"),"assigned_date"), + (_("Returned Date"),"return_date"), + (_("Asset"),"asset_id"), + (_("Return Description"),"return_condition"), + (_("Assign Condition Images"),"assign_condition_img",True), + (_("Return Condition Images"),"return_condition_img",True) + ] diff --git a/asset/cbv/asset_tab.py b/asset/cbv/asset_tab.py new file mode 100644 index 000000000..0f17821ed --- /dev/null +++ b/asset/cbv/asset_tab.py @@ -0,0 +1,119 @@ +""" +This page is handling the cbv methods of asset tab in profile page. +""" + +from typing import Any +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from asset.cbv.request_and_allocation import AllocationList, AssetRequestList +from employee.models import Employee +from horilla_views.generic.cbv.views import HorillaTabView +from employee.cbv.employee_profile import EmployeeProfileView + + +class AssetTabListView(AllocationList): + """ + Asset tab in individual view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("assets-tab-list-view",kwargs= {'pk': pk} ) + self.view_id = "asset-div" + + + + columns = AllocationList.columns + [ + (_("Status"), "status_display"), + (_("Assigned Date"), "assigned_date_display"), + ] + + def get_queryset(self): + """ + Returns a filtered queryset of records assigned to a specific employee + """ + + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(assigned_to_employee_id=pk).exclude( + return_status__isnull=False + ) + return queryset + + +class AssetRequestTab(AssetRequestList): + """ + Asset request tab + """ + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("asset-request-tab-list-view",kwargs= {'pk': pk} ) + self.view_id = "asset-request-div" + + def get_queryset(self): + """ + Returns a filtered queryset of records for the requested employee. + """ + + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(requested_employee_id=pk) + return queryset + + +class AssetTabView(HorillaTabView): + """ + generic tab view for asset tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "asset-tab" + + def get_context_data(self, **kwargs): + """ + Adds the employee details and tab information to the context. + """ + + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + context["emp_id"] = pk + employee = Employee.objects.get(id=pk) + context["instance"] = employee + context["tabs"] = [ + { + "title": _("Assets"), + "url": f"{reverse('assets-tab-list-view',kwargs={'pk': pk})}", + }, + { + "title": _("Asset Request"), + "url": f"{reverse('asset-request-tab-list-view',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Create Request", + "accessibility" :"asset.cbv.accessibility.create_asset_request_accessibility", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('asset-request-creation')}?pk={pk}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + } + ], + }, + ] + return context + + +EmployeeProfileView.add_tab( + tabs=[ + { + "title": "Asset", + "view": AssetTabView.as_view(), + "accessibility": "asset.cbv.accessibility.asset_accessibility", + }, + ] +) diff --git a/asset/cbv/dashboard.py b/asset/cbv/dashboard.py new file mode 100644 index 000000000..2bbb78ede --- /dev/null +++ b/asset/cbv/dashboard.py @@ -0,0 +1,78 @@ +from typing import Any +from django.urls import reverse +from django.utils.decorators import method_decorator +from asset.cbv.request_and_allocation import AssetAllocationList, AssetRequestList +from base.methods import filtersubordinates +from horilla_views.generic.cbv.views import HorillaListView +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required,name="dispatch") +class AssetRequestToApprove(AssetRequestList): + """ + Asset request to approve in dashboard + """ + + columns = [ + column for column in AssetRequestList.columns if column[1] != "status_col" + ] + + bulk_select_option = False + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-asset-request-approve") + + def get_queryset(self): + queryset = HorillaListView.get_queryset(self) + queryset = queryset.filter( + asset_request_status="Requested", requested_employee_id__is_active=True + ) + queryset = filtersubordinates( + self.request, + queryset, + "asset.change_assetrequest", + field="requested_employee_id", + ) + return queryset + + header_attrs = { + "requested_employee_id": """ + style ="width:100px !important" + """, + "asset_category_id": """ + style ="width:100px !important" + """, + "asset_request_date": """ + style ="width:100px !important" + """, + "status_col": """ + style ="width:100px !important" + """, + "action": """ + style ="width:100px !important" + """ + } + +@method_decorator(login_required,name="dispatch") +@method_decorator(permission_required("asset.view_assetcategory"),name="dispatch") +class AllocatedAssetsList(AssetAllocationList): + """ + List of allocated assets in dashboard + """ + + columns = [ + column + for column in AssetAllocationList.columns + if column[1] != "return_status_col" + ] + bulk_select_option = False + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter( + asset_id__asset_status="In use", assigned_to_employee_id__is_active=True + ) + return queryset + + action_method = None diff --git a/asset/cbv/request_and_allocation.py b/asset/cbv/request_and_allocation.py new file mode 100644 index 000000000..d56572559 --- /dev/null +++ b/asset/cbv/request_and_allocation.py @@ -0,0 +1,544 @@ +""" +Request and allocation page +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.contrib import messages +from asset.filters import AssetAllocationFilter, AssetRequestFilter, CustomAssetFilter +from asset.forms import AssetAllocationForm, AssetRequestForm +from asset.models import Asset, AssetAssignment, AssetRequest, ReturnImages +from base.methods import filtersubordinates +from employee.models import Employee +from notifications.signals import notify +from horilla.horilla_middlewares import _thread_locals +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + HorillaTabView, + TemplateView, +) + + +@method_decorator(login_required, name="dispatch") +class RequestAndAllocationView(TemplateView): + """ + for request and allocation page + """ + + template_name = "cbv/request_and_allocation/request_and_allocation.html" + + +@method_decorator(login_required, name="dispatch") +class AllocationList(HorillaListView): + """ + For both asset allocation and asset tab + """ + + # view_id = "view-container" + + bulk_update_fields = [ + "asset_id__expiry_date" + ] + + model = AssetAssignment + filter_class = AssetAllocationFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-asset") + + columns = [ + (_("Asset"), "asset_id__asset_name", "get_avatar"), + (_("Category"), "asset_id__asset_category_id"), + (_("Expiry Date"), "asset_id__expiry_date"), + ] + + + header_attrs = { + "action" : """ + style = "width:180px !important" + """, + "asset_id__asset_name" :""" + style = "width:250px !important" + """, + "asset_id__asset_category_id" : """ + style = "width:250px !important" + """, + "asset_id__expiry_date" : """ + style = "width:250px !important" + """, + } + + sortby_mapping = [ + ("Category", "asset_id__asset_category_id__asset_category_name"), + ("Expiry Date", "asset_id__expiry_date") + ] + + action_method = "asset_action" + + row_attrs = """ + hx-get='{detail_view_asset}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +class AssetList(AllocationList): + """ + Asset tab + """ + + # view_id = "assetlist" + def get_queryset(self): + """ + Returns a queryset of AssetRequest objects filtered by + the current user's employee ID. + """ + queryset = super().get_queryset() + employee = self.request.user.employee_get + queryset = queryset.filter(assigned_to_employee_id=employee).exclude( + return_status__isnull=False + ) + return queryset + + selected_instances_key_id = "assetlistInstances" + + + + +@method_decorator(login_required, name="dispatch") +class AssetAllocationList(AllocationList): + """ + Asset allocation tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-asset-allocation") + + columns = [ + ( + _("Allocated User"), + "assigned_to_employee_id", + "assigned_to_employee_id__get_avatar", + ), + (_("Asset"), "asset_id"), + (_("Assigned Date"), "assigned_date"), + (_("Return Date"), "return_status_col"), + ] + + sortby_mapping = [ + ("Allocated User","assigned_to_employee_id__get_full_name"), + ("Asset", "asset_id__asset_name"), + ("Assigned Date", "assigned_date"), + ("Return Date", "return_status_col"), + ] + + action_method = "allocation_action" + + row_attrs = """ + hx-get='{detail_view_asset_allocation}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +class AssetRequestList(HorillaListView): + """ + Asset Request Tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.search_url = reverse("list-asset-request") + # self.view_id = "view-container" + if self.request.user.has_perm("asset.add_assetassignment"): + self.action_method = "action_col" + + model = AssetRequest + filter_class = AssetRequestFilter + + def get_queryset(self): + """ + Returns a filtered queryset of AssetRequest objects + based on user permissions and employee ID. + """ + + queryset = super().get_queryset() + queryset = filtersubordinates( + request=self.request, + perm="asset.view_assetrequest", + queryset=queryset, + field="requested_employee_id", + ) | queryset.filter(requested_employee_id=self.request.user.employee_get) + return queryset + + columns = [ + ( + _("Request User"), + "requested_employee_id", + "requested_employee_id__get_avatar", + ), + (_("Asset Category"), "asset_category_id"), + (_("Requested Date"), "asset_request_date"), + (_("Status"), "status_col"), + ] + + header_attrs = { + "action" : """ + style = "width:180px !important" + """ + } + + sortby_mapping = [ + ("Request User","requested_employee_id__get_full_name"), + ("Asset Category", "asset_category_id__asset_category_name"), + ("Requested Date", "asset_request_date"), + ("Status", "status_col"), + ] + + row_attrs = """ + hx-get='{detail_view_asset_request}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +class RequestAndAllocationTab(HorillaTabView): + """ + Tab View + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.tabs = [ + { + "title": _("Asset"), + "url": f"{reverse('list-asset')}", + }, + { + "title": _("Asset Request"), + "url": f"{reverse('list-asset-request')}", + "actions": [ + { + "action": "Create Request", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('asset-request-creation')}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + } + ], + }, + ] + if self.request.user.has_perm("asset.view_assetassignment"): + self.tabs.append( + { + "title": _("Asset Allocation"), + "url": f"{reverse('list-asset-allocation')}", + "actions": [ + { + "action": "Create Allocation", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('asset-allocate-creation')}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + } + ], + }, + ) + + +@method_decorator(login_required, name="dispatch") +class RequestAndAllocationNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("tab-asset-request-allocation") + + nav_title = _("Asset") + filter_instance = AssetAllocationFilter() + filter_form_context_name = "asset_allocation_filter_form" + filter_body_template = "cbv/request_and_allocation/filter.html" + search_swap_target = "#listContainer" + + def get_context_data(self, **kwargs): + """ + context data + """ + context = super().get_context_data(**kwargs) + assets_filter_form = CustomAssetFilter() + asset_request_filter_form = AssetRequestFilter() + context["assets_filter_form"] = assets_filter_form.form + context["asset_request_filter_form"] = asset_request_filter_form.form + return context + + group_by_fields = [ + ("requested_employee_id", _("Asset Request / Employee")), + ("asset_category_id", _("Asset Request / Asset Category")), + ("asset_request_date", _("Asset Request / Request Date")), + ("asset_request_status", _("Asset Request / Status")), + ("assigned_to_employee_id", _("Asset Allocation / Employee")), + ("assigned_date", _("Asset Allocation / Assigned Date")), + ("return_date", _("Asset Allocation / Return Date")), + ] + + +@method_decorator(login_required, name="dispatch") +class AssetDetailView(HorillaDetailedView): + """ + detail view of asset tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + (_("Description"), "asset_id__asset_description"), + (_("Tracking Id"), "asset_id__asset_tracking_id"), + (_("Assigned Date"), "assigned_date"), + (_("Status"), "asset_detail_status"), + (_("Assigned by"), "assigned_by_employee_id"), + (_("Batch No"), "asset_id__asset_lot_number_id"), + # ("Category","asset_id__asset_category_id") + ] + + action_method = "asset_detail_action" + + model = AssetAssignment + title = _("Asset Information") + header = { + "title": "asset_id__asset_name", + "subtitle": "asset_id__asset_category_id", + "avatar": "get_avatar", + } + + +@method_decorator(login_required, name="dispatch") +class AssetRequestDetailView(HorillaDetailedView): + """ + detail view of asset request tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + (_("Asset Category"), "asset_category_id"), + (_("Requested Date"), "asset_request_date"), + (_("Request Description"), "description"), + (_("Status"), "status_col"), + ] + + model = AssetRequest + title = _("Details") + header = { + "title": "requested_employee_id", + "subtitle": "asset_request_detail_subtitle", + "avatar": "requested_employee_id__get_avatar", + } + action_method = "detail_action_col" + + +@method_decorator(login_required, name="dispatch") +class AssetAllocationDetailView(HorillaDetailedView): + """ + detail view of asset allocation tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + (_("Returned Status"), "return_status"), + (_("Allocated User"), "assigned_by_employee_id"), + (_("Allocated Date"), "assigned_date"), + (_("Return Date"), "return_date"), + (_("Asset"), "asset_id"), + (_("Return Description"), "return_condition"), + (_("Status"), "detail_status"), + ] + + model = AssetAssignment + title = _("Details") + header = { + "title": "assigned_to_employee_id", + "subtitle": "asset_allocation_detail_subtitle", + "avatar": "assigned_to_employee_id__get_avatar", + } + action_method = "asset_allocation_detail_action" + + +@method_decorator(login_required, name="dispatch") +class AssetRequestCreateForm(HorillaFormView): + """ + Create Asset request + """ + + model = AssetRequest + form_class = AssetRequestForm + template_name = "cbv/request_and_allocation/forms/req_form.html" + new_display_title = _("Asset Request") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.request.GET.get('pk'): + pk = self.request.GET.get('pk') + self.form.fields["requested_employee_id"].queryset = Employee.objects.filter( + id=pk + ) + self.form.fields["requested_employee_id"].initial = pk + return context + + def form_valid(self, form: AssetRequestForm) -> HttpResponse: + """ + Handles validation and saving of an AssetRequestForm. + """ + if form.is_valid(): + message = _("Asset Request Created Successfully") + form.save() + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="asset.add_asset"),name="dispatch") +class AssetAllocationFormView(HorillaFormView): + """ + Create Asset Allocation + """ + + model = AssetAssignment + form_class = AssetAllocationForm + template_name = "cbv/request_and_allocation/forms/allo_form.html" + new_display_title = _("Asset Allocation") + + + + def form_valid(self, form: AssetAllocationForm) -> HttpResponse: + """ + form valid function + """ + if form.is_valid(): + asset = form.instance.asset_id + asset.asset_status = "In use" + asset.save() + message = _("Asset allocated Successfully") + form.save() + request = getattr(_thread_locals, "request", None) + files = request.FILES.getlist("asset_condition_img") + attachments = [] + if request.FILES: + for file in files: + attachment = ReturnImages() + attachment.image = file + attachment.save() + attachments.append(attachment) + form.instance.assign_images.add(*attachments) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +class AssetApproveFormView(HorillaFormView): + """ + Create Asset Allocation + """ + + model = AssetAssignment + form_class = AssetAllocationForm + template_name = "cbv/request_and_allocation/forms/asset_approve_form.html" + new_display_title = _("Asset Allocation") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + req_id = self.kwargs.get('req_id') + asset_request = AssetRequest.objects.filter(id=req_id).first() + asset_category = asset_request.asset_category_id + assets = asset_category.asset_set.filter(asset_status="Available") + self.form.fields["asset_id"].queryset = assets + self.form.fields["assigned_to_employee_id"].initial = asset_request.requested_employee_id + self.form.fields["assigned_by_employee_id"].initial = self.request.user.employee_get + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + + def form_valid(self, form: AssetAllocationForm) -> HttpResponse: + """ + form valid function + """ + req_id = self.kwargs.get('req_id') + asset_request = AssetRequest.objects.filter(id=req_id).first() + if form.is_valid(): + asset = form.instance.asset_id.id + asset = Asset.objects.filter(id=asset).first() + asset.asset_status = "In use" + asset.save() + # form = form.save(commit=False) + # form.assigned_by_employee_id = self.request.user.employee_get + form.save() + asset_request.asset_request_status = "Approved" + asset_request.save() + request = getattr(_thread_locals, "request", None) + files = request.FILES.getlist("asset_condition_img") + attachments = [] + if request.FILES: + for file in files: + attachment = ReturnImages() + attachment.image = file + attachment.save() + attachments.append(attachment) + form.instance.assign_images.add(*attachments) + messages.success(self.request, _("Asset request approved successfully!.")) + notify.send( + self.request.user.employee_get, + recipient=asset_request.requested_employee_id.employee_user_id, + verb="Your asset request approved!.", + verb_ar="تم الموافقة على طلب الأصول الخاص بك!", + verb_de="Ihr Antragsantrag wurde genehmigt!", + verb_es="¡Su solicitud de activo ha sido aprobada!", + verb_fr="Votre demande d'actif a été approuvée !", + redirect=reverse("asset-request-allocation-view") + + f"?asset_request_date={asset_request.asset_request_date}\ + &asset_request_status={asset_request.asset_request_status}", + icon="bag-check", + ) + return HttpResponse("") + return super().form_valid(form) + + + + + + + + + diff --git a/asset/filters.py b/asset/filters.py index 1a89bcea1..d89724bb6 100644 --- a/asset/filters.py +++ b/asset/filters.py @@ -10,11 +10,12 @@ from django.db.models import Q from django_filters import FilterSet from base.methods import reload_queryset +from horilla.filters import HorillaFilterSet -from .models import Asset, AssetAssignment, AssetCategory, AssetRequest +from .models import Asset, AssetAssignment, AssetCategory, AssetLot, AssetRequest -class CustomFilterSet(FilterSet): +class CustomFilterSet(HorillaFilterSet): """ Custom FilterSet class that applies specific CSS classes to filter widgets. @@ -373,3 +374,14 @@ class AssetHistoryReGroup: ("assigned_date", "Assigned Date"), ("return_date", "Return Date"), ] + + +class AssetBatchNoFilter(FilterSet): + + search = django_filters.CharFilter(field_name="lot_number", lookup_expr="icontains") + + class Meta: + model = AssetLot + fields = [ + "lot_number", + ] diff --git a/asset/forms.py b/asset/forms.py index e19e482d2..84a20b379 100644 --- a/asset/forms.py +++ b/asset/forms.py @@ -60,6 +60,17 @@ class AssetForm(ModelForm): attrs={"onchange": "batchNoChange($(this))"} ), } + labels = { + "asset_name": "Asset Name", + "asset_description": "Description", + "asset_tracking_id": "Tracking ID", + "asset_purchase_date": "Purchase Date", + "expiry_date": "Expiry Date", + "asset_purchase_cost": "Cost", + "asset_category_id": "Category", + "asset_status": "Status", + "asset_lot_number_id": "Batch Number", + } def __init__(self, *args, **kwargs): request = getattr(_thread_locals, "request", None) @@ -165,6 +176,19 @@ class AssetReportForm(ModelForm): - __init__: Initializes the form, disabling the 'asset_id' field. """ + file = forms.FileField( + required=False, + widget=forms.TextInput( + attrs={ + "name": "file", + "type": "File", + "class": "form-control", + "multiple": "True", + "accept": ".jpeg, .jpg, .png, .pdf", + } + ), + ) + class Meta: """ Metadata options for the AssetReportForm. @@ -176,10 +200,7 @@ class AssetReportForm(ModelForm): """ model = AssetReport - fields = [ - "title", - "asset_id", - ] + fields = ["title", "asset_id", "file"] exclude = ["is_active"] def __init__(self, *args, **kwargs): @@ -191,7 +212,7 @@ class AssetReportForm(ModelForm): - **kwargs: Arbitrary keyword arguments. """ super().__init__(*args, **kwargs) - self.fields["asset_id"].widget.attrs["disabled"] = "disabled" + # self.fields["asset_id"].widget.attrs["disabled"] = "disabled" class AssetCategoryForm(ModelForm): @@ -199,6 +220,12 @@ class AssetCategoryForm(ModelForm): A form for creating and updating AssetCategory instances. """ + cols = { + "asset_category_name": 12, + "asset_category_description": 12, + "company_id": 12, + } + class Meta: """ Specifies the model and fields to be used for the AssetForm. @@ -218,6 +245,8 @@ class AssetRequestForm(ModelForm): A Django ModelForm for creating and updating AssetRequest instances. """ + cols = {"requested_employee_id": 12, "asset_category_id": 12, "description": 12} + class Meta: """ Specifies the model and fields to be used for the AssetRequestForm. @@ -258,10 +287,16 @@ class AssetRequestForm(ModelForm): } def __init__(self, *args, **kwargs): - user = kwargs.pop("user", None) - super().__init__(*args, **kwargs) + # user = kwargs.pop("user", None) + request = getattr(_thread_locals, "request", None) + user = request.user + super(AssetRequestForm, self).__init__( + *args, + **kwargs, + ) reload_queryset(self.fields) if user is not None and user.has_perm("asset.add_assetrequest"): + self.fields["requested_employee_id"].queryset = Employee.objects.all() self.fields["requested_employee_id"].initial = Employee.objects.filter( id=user.employee_get.id @@ -280,8 +315,16 @@ class AssetAllocationForm(ModelForm): A Django ModelForm for creating and updating AssetAssignment instances. """ + cols = { + "assigned_to_employee_id": 12, + "asset_id": 12, + "assigned_by_employee_id": 12, + } + def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + user = request.user + super(AssetAllocationForm, self).__init__(*args, **kwargs) reload_queryset(self.fields) self.fields["asset_id"].queryset = Asset.objects.filter( asset_status="Available" @@ -310,6 +353,7 @@ class AssetAllocationForm(ModelForm): "return_condition", "assigned_date", "return_images", + "assign_images", "is_active", ] widgets = { @@ -403,6 +447,8 @@ class AssetBatchForm(ModelForm): A Django ModelForm for creating or updating AssetLot instances. """ + cols = {"lot_description": 12, "lot_number": 12} + class Meta: """ Specifies the model and fields to be used for the AssetBatchForm. diff --git a/asset/models.py b/asset/models.py index 64dc056dc..2d9796f8a 100644 --- a/asset/models.py +++ b/asset/models.py @@ -7,12 +7,15 @@ within an Asset Management System. from django.core.exceptions import ValidationError from django.db import models +from django.urls import reverse, reverse_lazy +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company from employee.models import Employee from horilla.models import HorillaModel +from horilla_views.cbv_methods import render_template class AssetCategory(HorillaModel): @@ -72,6 +75,49 @@ class AssetLot(HorillaModel): def __str__(self): return f"{self.lot_number}" + def actions(self): + """ + This method for get custom column for action. + """ + + return render_template( + path="cbv/asset_batch_no/actions.html", + context={"instance": self}, + ) + + def asset_batch_detail(self): + """ + detail view + """ + + url = reverse("asset-batch-detail-view", kwargs={"pk": self.pk}) + + return url + + def assets_column(self): + """ + This method for get custom column for action. + """ + + return render_template( + path="cbv/asset_batch_no/assets_col.html", + context={"instance": self}, + ) + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("asset-batch-update", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("asset-batch-number-delete", kwargs={"batch_id": self.pk}) + return url + class Asset(HorillaModel): """ @@ -131,6 +177,36 @@ class Asset(HorillaModel): def __str__(self): return f"{self.asset_name}-{self.asset_tracking_id}" + def get_status_display(self): + """ + Display status + """ + return dict(self.ASSET_STATUS).get(self.asset_status) + + def detail_view_action(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/asset_category/detail_view_action.html", + context={"instance": self}, + ) + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("asset-update", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("asset-delete", kwargs={"asset_id": self.pk}) + return url + def clean(self): existing_asset = Asset.objects.filter( asset_tracking_id=self.asset_tracking_id @@ -274,6 +350,162 @@ class AssetAssignment(HorillaModel): def __str__(self): return f"{self.assigned_to_employee_id} --- {self.asset_id} --- {self.return_status}" + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.asset_id}&background=random" + return url + + def asset_detail_view(self): + """ + for detail view of page + """ + url = reverse("asset-history-detail-view", kwargs={"pk": self.pk}) + return url + + def assign_condition_img(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/asset_history/assign_condition.html", + context={"instance": self}, + ) + + def return_condition_img(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/asset_history/return_condition.html", + context={"instance": self}, + ) + + def asset_action(self): + """ + This method for get custom column for asset tab action. + """ + + return render_template( + path="cbv/request_and_allocation/asset_actions.html", + context={"instance": self}, + ) + + def return_status_col(self): + """ + This method for get custom column for return date. + """ + + return render_template( + path="cbv/request_and_allocation/return_status.html", + context={"instance": self}, + ) + + def allocation_action(self): + """ + This method for get custom column for asset allocation tab actions. + """ + + return render_template( + path="cbv/request_and_allocation/asset_allocation_action.html", + context={"instance": self}, + ) + + def asset_detail_action(self): + """ + This method for get custom column for asset detail actions. + """ + + return render_template( + path="cbv/request_and_allocation/asset_detail_action.html", + context={"instance": self}, + ) + + def asset_allocation_detail_action(self): + """ + This method for get custom column for asset detail actions. + """ + + return render_template( + path="cbv/request_and_allocation/detail_action_asset_allocation.html", + context={"instance": self}, + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the question template + """ + url = f"https://ui-avatars.com/api/?name={self.asset_id.asset_name}&background=random" + return url + + def detail_view_asset(self): + """ + detail view + """ + + url = reverse("asset-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_view_asset_allocation(self): + """ + detail view + """ + + url = reverse("asset-allocation-detail-view", kwargs={"pk": self.pk}) + return url + + def asset_detail_status(self): + """ + Asset tab detail status + """ + + return ( + 'Requested to return' + if self.return_request + else 'In use' + ) + + def detail_status(self): + """ + Asset allocation tab detail status + """ + if self.return_date: + status = 'Returned' + elif self.return_request: + status = 'Requested to return' + else: + status = 'Allocated' + return status + + def asset_allocation_detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.assigned_to_employee_id.employee_work_info.department_id} / {self.assigned_to_employee_id.employee_work_info.job_position_id}" + + def status_display(self): + status = self.asset_id.asset_status + color_class = "oh-dot--warning" # Adjust based on your status + return format_html( + '' + '{status}', + color_class=color_class, + status=status, + ) + + def assigned_date_display(self): + date_col = self.assigned_date + color_class = "oh-dot--success" # Adjust based on your status + return format_html( + '' + '{date_col}', + color_class=color_class, + date_col=date_col, + ) + class AssetRequest(HorillaModel): """ @@ -314,6 +546,49 @@ class AssetRequest(HorillaModel): verbose_name = _("Asset Request") verbose_name_plural = _("Asset Requests") + def status_col(self): + """ + This method for get custom coloumn for status. + """ + + return render_template( + path="cbv/request_and_allocation/status.html", + context={"instance": self}, + ) + + def action_col(self): + """ + This method for get custom coloumn for action. + """ + + return render_template( + path="cbv/request_and_allocation/asset_request_action.html", + context={"instance": self}, + ) + + def detail_action_col(self): + """ + This method for get custom coloumn for detail action. + """ + + return render_template( + path="cbv/request_and_allocation/asset_request_detail_action.html", + context={"instance": self}, + ) + + def asset_request_detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.requested_employee_id.employee_work_info.department_id} / {self.requested_employee_id.employee_work_info.job_position_id}" + + def detail_view_asset_request(self): + """ + detail view + """ + url = reverse("asset-request-detail-view", kwargs={"pk": self.pk}) + return url + def status_html_class(self): COLOR_CLASS = { "Approved": "oh-dot--success", diff --git a/asset/templates/asset/asset_information.html b/asset/templates/asset/asset_information.html index e45792d07..331553419 100644 --- a/asset/templates/asset/asset_information.html +++ b/asset/templates/asset/asset_information.html @@ -133,8 +133,8 @@
{% if perms.asset.change_asset %} - {% trans "Edit" %} diff --git a/asset/templates/asset/asset_list.html b/asset/templates/asset/asset_list.html index 7c29e7c44..772b0f75d 100644 --- a/asset/templates/asset/asset_list.html +++ b/asset/templates/asset/asset_list.html @@ -37,9 +37,9 @@
diff --git a/asset/templates/category/asset_category.html b/asset/templates/category/asset_category.html index 63176ab4d..92acc3629 100644 --- a/asset/templates/category/asset_category.html +++ b/asset/templates/category/asset_category.html @@ -72,7 +72,7 @@
  • {% trans "Edit" %}
  • {% endif %} diff --git a/asset/templates/category/asset_category_view.html b/asset/templates/category/asset_category_view.html index ad0b08263..61190dd43 100644 --- a/asset/templates/category/asset_category_view.html +++ b/asset/templates/category/asset_category_view.html @@ -43,8 +43,9 @@ {% endif %} - - +{% include "generic/components.html" %} +{% comment %}
    +
    {% endcomment %}
    @@ -101,10 +102,11 @@
    - - + + + -->
    @@ -191,11 +193,13 @@
    {% if perms.asset.add_assetcategory %}
    - - - {% trans "Create" %} + + + {% trans "Create" %}
    {% endif %} diff --git a/asset/templates/cbv/asset_batch_no/actions.html b/asset/templates/cbv/asset_batch_no/actions.html new file mode 100644 index 000000000..c3f6600c1 --- /dev/null +++ b/asset/templates/cbv/asset_batch_no/actions.html @@ -0,0 +1,45 @@ +{% load i18n %} +{% if perms.asset.change_assetlot or perms.asset.delete_assetlot %} +
    +
    + {% if perms.asset.change_assetlot %} + + + + {% endif %} + {% if perms.asset.delete_assetlot %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    + {% endif%} \ No newline at end of file diff --git a/asset/templates/cbv/asset_batch_no/asset_batch_no.html b/asset/templates/cbv/asset_batch_no/asset_batch_no.html new file mode 100644 index 000000000..e9211df61 --- /dev/null +++ b/asset/templates/cbv/asset_batch_no/asset_batch_no.html @@ -0,0 +1,43 @@ +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + + + + + +
    +
    + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/asset/templates/cbv/asset_batch_no/assets_col.html b/asset/templates/cbv/asset_batch_no/assets_col.html new file mode 100644 index 000000000..4a41b29f5 --- /dev/null +++ b/asset/templates/cbv/asset_batch_no/assets_col.html @@ -0,0 +1,5 @@ +{% load i18n %} + + {{instance.asset_set.count}} + {% trans "Assets" %} + \ No newline at end of file diff --git a/asset/templates/cbv/asset_category/detail_view_action.html b/asset/templates/cbv/asset_category/detail_view_action.html new file mode 100644 index 000000000..658811c57 --- /dev/null +++ b/asset/templates/cbv/asset_category/detail_view_action.html @@ -0,0 +1,19 @@ +{% load i18n %} +{% include 'generic/horilla_detailed_view.html'%} + +{% if messages %} +
    + {% for message in messages %} +
    +
    + {{ message }} +
    +
    + +{%if message.tags == "oh-alert--success" %} + + {% endif %} + {% endfor %} +
    +{% endif %} \ No newline at end of file diff --git a/asset/templates/cbv/asset_category/filter.html b/asset/templates/cbv/asset_category/filter.html new file mode 100644 index 000000000..fd2ac9588 --- /dev/null +++ b/asset/templates/cbv/asset_category/filter.html @@ -0,0 +1,53 @@ +{% load i18n %} +{% load assets_custom_filter %} +{% load widget_tweaks %} +
    +
    +
    {% trans "Asset" %}
    +
    + {% comment %}
    {% endcomment %} +
    +
    +
    + + {{form.asset_name}} +
    +
    + + {{form.asset_tracking_id}} +
    + +
    +
    +
    + + {{form.asset_purchase_date |attr:"type:date"}} +
    +
    + + {{form.asset_purchase_cost}} +
    +
    +
    +
    + + {{form.asset_lot_number_id}} +
    +
    +
    +
    + + {{form.asset_category_id}} +
    +
    +
    +
    + + {{form.asset_status}} +
    +
    +
    +
    +
    + {% comment %} {% endcomment %} +
    \ No newline at end of file diff --git a/asset/templates/cbv/asset_history/asset_history_filter.html b/asset/templates/cbv/asset_history/asset_history_filter.html new file mode 100644 index 000000000..daeb6849e --- /dev/null +++ b/asset/templates/cbv/asset_history/asset_history_filter.html @@ -0,0 +1,112 @@ +{% load static %} +{% load i18n %} +{% load mathfilters %} +{% load widget_tweaks %} +
    +
    +
    {% trans "Asset History" %}
    +
    +
    +
    +
    + + {{form.assigned_to_employee_id}} +
    +
    + + {{form.asset_id}} +
    +
    +
    +
    + + {{ form.assigned_date |attr:"type:date" }} +
    +
    + + {{form.return_status}} +
    +
    +
    +
    + + {{ form.return_date|attr:"type:date" }} +
    +
    +
    +
    + + {{form.assigned_by_employee_id}} +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.return_date_gte}} +
    +
    + + {{form.assigned_date_gte}} +
    +
    + +
    +
    + + {{form.return_date_lte}} +
    +
    + + {{form.assigned_date_lte}} +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/asset/templates/cbv/asset_history/asset_history_home.html b/asset/templates/cbv/asset_history/asset_history_home.html new file mode 100644 index 000000000..c38d5e27c --- /dev/null +++ b/asset/templates/cbv/asset_history/asset_history_home.html @@ -0,0 +1,118 @@ +{% extends "index.html" %} +{% load i18n %} +{% load static %} +{% block content %} + + + +
    +
    + +{% include "generic/components.html" %} + + +
    +
    +
    +
    + + + + +{% endblock content %} \ No newline at end of file diff --git a/asset/templates/cbv/asset_history/assign_condition.html b/asset/templates/cbv/asset_history/assign_condition.html new file mode 100644 index 000000000..17606877d --- /dev/null +++ b/asset/templates/cbv/asset_history/assign_condition.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% for doc in instance.assign_images.all %} +{% trans "Assign Condition Images" %} +
    + + {% endfor %} +
    diff --git a/asset/templates/cbv/asset_history/return_condition.html b/asset/templates/cbv/asset_history/return_condition.html new file mode 100644 index 000000000..8586a37e3 --- /dev/null +++ b/asset/templates/cbv/asset_history/return_condition.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% for doc in instance.return_images.all %} +{% trans "Return Condition Images" %} +
    + + {% endfor %} +
    diff --git a/asset/templates/cbv/request_and_allocation/asset_actions.html b/asset/templates/cbv/request_and_allocation/asset_actions.html new file mode 100644 index 000000000..a13649797 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/asset_actions.html @@ -0,0 +1,32 @@ +{% load i18n %} +{% if perms.asset.change_assetassignment %} +
    + +{% else %} + {% if instance.return_request %} +
    + + {% trans "Requested to return" %} +
    + {% else %} +
    + {% csrf_token %} + +
    + {% endif %} +{% endif %} +
    diff --git a/asset/templates/cbv/request_and_allocation/asset_allocation_action.html b/asset/templates/cbv/request_and_allocation/asset_allocation_action.html new file mode 100644 index 000000000..7cac37ea8 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/asset_allocation_action.html @@ -0,0 +1,25 @@ +{% load i18n %} +{% if not instance.return_status %} +
    + +
    +{% else %} +
    +
    + + {% trans "Returned" %} +
    +
    +{% endif %} \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/asset_detail_action.html b/asset/templates/cbv/request_and_allocation/asset_detail_action.html new file mode 100644 index 000000000..96ad86cc8 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/asset_detail_action.html @@ -0,0 +1,32 @@ +{% load i18n %} +
    + {% if perms.asset.change_assetassignment or not instance.return_request %} +
    + {% if perms.asset.change_assetassignment %} + + {% else %} + {% if not instance.return_request %} +
    + {% csrf_token %} + +
    + {% endif %} + {% endif %} +
    + {% endif %} +
    \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/asset_request_action.html b/asset/templates/cbv/request_and_allocation/asset_request_action.html new file mode 100644 index 000000000..0381a7186 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/asset_request_action.html @@ -0,0 +1,56 @@ +{% load i18n %} +{% if perms.asset.add_assetassignment %} + {% if instance.asset_request_status == 'Requested' %} +
    +
    + + + +
    + {% csrf_token %} + +
    +
    +
    + {% else %} +
    + +
    + {% endif %} +{% endif %} diff --git a/asset/templates/cbv/request_and_allocation/asset_request_detail_action.html b/asset/templates/cbv/request_and_allocation/asset_request_detail_action.html new file mode 100644 index 000000000..f076a5a7d --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/asset_request_detail_action.html @@ -0,0 +1,30 @@ +{% load i18n %} +
    + {% if perms.asset.add_assetassignment %} + {% if instance.asset_request_status == 'Requested' %} +
    + + {% trans 'Approve' %} + +
    + {% csrf_token %} + +
    +
    + {% endif %} + {% endif %} +
    \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/detail_action_asset_allocation.html b/asset/templates/cbv/request_and_allocation/detail_action_asset_allocation.html new file mode 100644 index 000000000..d22d59942 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/detail_action_asset_allocation.html @@ -0,0 +1,15 @@ +{% load i18n %} +
    + {% if not instance.return_status %} + + {% endif %} +
    \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/filter.html b/asset/templates/cbv/request_and_allocation/filter.html new file mode 100644 index 000000000..8e10f429c --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/filter.html @@ -0,0 +1,144 @@ +{% load i18n %} + +{% load widget_tweaks %} +
    +
    +
    {% trans "Asset" %}
    +
    +
    +
    +
    + + {{assets_filter_form.asset_id__asset_name}} +
    +
    +
    +
    + + {{assets_filter_form.asset_id__asset_status}} +
    +
    +
    +
    +
    +
    +
    + {% trans "Asset Request" %} +
    +
    +
    +
    +
    + + {{asset_request_filter_form.requested_employee_id}} +
    +
    + + {{asset_request_filter_form.asset_category_id}} +
    +
    +
    +
    + + {{asset_request_filter_form.asset_request_date|attr:"type:date"}} +
    +
    + + {{asset_request_filter_form.asset_request_status}} +
    +
    +
    +
    +
    + {% if perms.asset.view_assetassignment %} +
    +
    + {% trans "Asset Allocation" %} +
    +
    +
    +
    +
    + + {{asset_allocation_filter_form.assigned_to_employee_id}} +
    +
    + + {{asset_allocation_filter_form.asset_id}} +
    +
    +
    +
    + + {{ asset_allocation_filter_form.assigned_date |attr:"type:date" }} +
    +
    + + {{asset_allocation_filter_form.return_status}} +
    +
    +
    +
    + + {{ asset_allocation_filter_form.return_date|attr:"type:date" }} +
    +
    +
    +
    + + {{asset_allocation_filter_form.assigned_by_employee_id}} +
    +
    +
    +
    +
    + {% endif %} +
    \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/forms/allo_form.html b/asset/templates/cbv/request_and_allocation/forms/allo_form.html new file mode 100644 index 000000000..f7a09219a --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/forms/allo_form.html @@ -0,0 +1,11 @@ +
    + {% include "generic/horilla_form.html" %} +
    + \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/forms/asset_approve_form.html b/asset/templates/cbv/request_and_allocation/forms/asset_approve_form.html new file mode 100644 index 000000000..a0feb33a6 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/forms/asset_approve_form.html @@ -0,0 +1,11 @@ +
    + {% include "generic/horilla_form.html" %} +
    + \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/forms/req_form.html b/asset/templates/cbv/request_and_allocation/forms/req_form.html new file mode 100644 index 000000000..7828e3df3 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/forms/req_form.html @@ -0,0 +1,8 @@ +
    + {% include "generic/horilla_form.html" %} +
    + \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/request_and_allocation.html b/asset/templates/cbv/request_and_allocation/request_and_allocation.html new file mode 100644 index 000000000..5c264191d --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/request_and_allocation.html @@ -0,0 +1,132 @@ +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + + +
    +
    + +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/asset/templates/cbv/request_and_allocation/return_status.html b/asset/templates/cbv/request_and_allocation/return_status.html new file mode 100644 index 000000000..c43863c5d --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/return_status.html @@ -0,0 +1,20 @@ +{% load i18n %} +{% if instance.return_date %} +
    + {{instance.return_date}} +
    +{% else %} +{% if instance.return_request %} +
    +
    + + {% trans "Requested to return" %} +
    +
    +{% else %} +
    + + {% trans "Allocated" %} +
    +{% endif %} +{% endif %} diff --git a/asset/templates/cbv/request_and_allocation/status.html b/asset/templates/cbv/request_and_allocation/status.html new file mode 100644 index 000000000..7266b3883 --- /dev/null +++ b/asset/templates/cbv/request_and_allocation/status.html @@ -0,0 +1,10 @@ +{% load i18n %} +
    + + + {% trans instance.asset_request_status %} + +
    \ No newline at end of file diff --git a/asset/urls.py b/asset/urls.py index 310a24bbc..532f9543b 100644 --- a/asset/urls.py +++ b/asset/urls.py @@ -5,6 +5,14 @@ URL configuration for asset-related views. from django import views from django.urls import path +from asset.cbv import ( + asset_batch_no, + asset_category, + asset_history, + asset_tab, + dashboard, + request_and_allocation, +) from asset.forms import AssetCategoryForm, AssetForm from asset.models import Asset, AssetCategory from base.views import object_duplicate @@ -12,28 +20,66 @@ from base.views import object_duplicate from . import views urlpatterns = [ + path( + "asset-history/", asset_history.AssetHistoryView.as_view(), name="asset-history" + ), + path( + "asset-history-list/", + asset_history.AssetHistorylistView.as_view(), + name="asset-history-list", + ), + path( + "asset-history-nav/", + asset_history.AssetHistoryNavView.as_view(), + name="asset-history-nav", + ), + path( + "asset-history-detail-view//", + asset_history.AssetHistoryDetailView.as_view(), + name="asset-history-detail-view", + ), + # path( + # "asset-creation//", + # views.asset_creation, + # name="asset-creation", + # ), path( "asset-creation//", - views.asset_creation, + asset_category.AssetFormView.as_view(), name="asset-creation", ), path("asset-list/", views.asset_list, name="asset-list"), - path("asset-update//", views.asset_update, name="asset-update"), + # path("asset-update//", views.asset_update, name="asset-update"), + path( + "asset-update//", + asset_category.AssetFormView.as_view(), + name="asset-update", + ), + # path( + # "duplicate-asset//", + # object_duplicate, + # name="duplicate-asset", + # kwargs={ + # "model": Asset, + # "form": AssetForm, + # "form_name": "asset_creation_form", + # "template": "asset/asset_creation.html", + # }, + # ), path( "duplicate-asset//", - object_duplicate, + asset_category.AssetDuplicateFormView.as_view(), name="duplicate-asset", - kwargs={ - "model": Asset, - "form": AssetForm, - "form_name": "asset_creation_form", - "template": "asset/asset_creation.html", - }, ), path("asset-delete//", views.asset_delete, name="asset-delete"), + # path( + # "asset-information//", + # views.asset_information, + # name="asset-information", + # ), path( - "asset-information//", - views.asset_information, + "asset-information//", + asset_category.AssetCategoryDetailView.as_view(), name="asset-information", ), path("asset-category-view/", views.asset_category_view, name="asset-category-view"), @@ -42,9 +88,20 @@ urlpatterns = [ views.asset_category_view_search_filter, name="asset-category-view-search-filter", ), + # path( + # "asset-category-duplicate//", + # object_duplicate, + # name="asset-category-duplicate", + # kwargs={ + # "model": AssetCategory, + # "form": AssetCategoryForm, + # "form_name": "asset_category_form", + # "template": "category/asset_category_creation.html", + # }, + # ), path( "asset-category-duplicate//", - object_duplicate, + asset_category.AssetCategoryDuplicateFormView.as_view(), name="asset-category-duplicate", kwargs={ "model": AssetCategory, @@ -53,14 +110,29 @@ urlpatterns = [ "template": "category/asset_category_form.html", }, ), + path( + "asset-category-nav/", + asset_category.AssetCategoryNav.as_view(), + name="asset-category-nav", + ), + # path( + # "asset-category-creation", + # views.asset_category_creation, + # name="asset-category-creation", + # ), path( "asset-category-creation", - views.asset_category_creation, + asset_category.AssetCategoryFormView.as_view(), name="asset-category-creation", ), + # path( + # "asset-category-update/", + # views.asset_category_update, + # name="asset-category-update", + # ), path( - "asset-category-update/", - views.asset_category_update, + "asset-category-update/", + asset_category.AssetCategoryFormView.as_view(), name="asset-category-update", ), path( @@ -68,16 +140,21 @@ urlpatterns = [ views.delete_asset_category, name="asset-category-delete", ), + # path( + # "asset-request-creation", + # views.asset_request_creation, + # name="asset-request-creation", + # ), path( "asset-request-creation", - views.asset_request_creation, + request_and_allocation.AssetRequestCreateForm.as_view(), name="asset-request-creation", ), - path( - "asset-request-allocation-view/", - views.asset_request_allocation_view, - name="asset-request-allocation-view", - ), + # path( + # "asset-request-allocation-view/", + # views.asset_request_allocation_view, + # name="asset-request-allocation-view", + # ), path( "asset-request-individual-view/", views.asset_request_individual_view, @@ -108,9 +185,14 @@ urlpatterns = [ views.asset_request_reject, name="asset-request-reject", ), + # path( + # "asset-allocate-creation", + # views.asset_allocate_creation, + # name="asset-allocate-creation", + # ), path( "asset-allocate-creation", - views.asset_allocate_creation, + request_and_allocation.AssetAllocationFormView.as_view(), name="asset-allocate-creation", ), path( @@ -126,20 +208,50 @@ urlpatterns = [ path("asset-excel", views.asset_excel, name="asset-excel"), path("asset-import", views.asset_import, name="asset-import"), path("asset-export-excel", views.asset_export_excel, name="asset-export-excel"), + # path( + # "asset-batch-number-creation", + # views.asset_batch_number_creation, + # name="asset-batch-number-creation", + # ), + # path("asset-batch-view", views.asset_batch_view, name="asset-batch-view"), path( "asset-batch-number-creation", - views.asset_batch_number_creation, + asset_batch_no.AssetBatchCreateFormView.as_view(), name="asset-batch-number-creation", ), - path("asset-batch-view", views.asset_batch_view, name="asset-batch-view"), + path( + "asset-batch-view", + asset_batch_no.AssetBatchNoView.as_view(), + name="asset-batch-view", + ), + path( + "asset-batch-list", + asset_batch_no.AssetBatchNoListView.as_view(), + name="asset-batch-list", + ), + path( + "asset-batch-nav", + asset_batch_no.AssetBatchNoNav.as_view(), + name="asset-batch-nav", + ), + path( + "asset-batch-detail-view//", + asset_batch_no.AssetBatchDetailView.as_view(), + name="asset-batch-detail-view", + ), path( "asset-batch-number-search", views.asset_batch_number_search, name="asset-batch-number-search", ), + # path( + # "asset-batch-update/", + # views.asset_batch_update, + # name="asset-batch-update", + # ), path( - "asset-batch-update/", - views.asset_batch_update, + "asset-batch-update/", + asset_batch_no.AssetBatchCreateFormView.as_view(), name="asset-batch-update", ), path( @@ -149,9 +261,14 @@ urlpatterns = [ ), path("asset-count-update", views.asset_count_update, name="asset-count-update"), path("add-asset-report/", views.add_asset_report, name="add-asset-report"), + # path( + # "add-asset-report/", + # views.add_asset_report, + # name="add-asset-report", + # ), path( - "add-asset-report/", - views.add_asset_report, + "add-asset-report//", + asset_category.AssetReportFormView.as_view(), name="add-asset-report", ), path("dashboard/", views.asset_dashboard, name="asset-dashboard"), @@ -173,11 +290,11 @@ urlpatterns = [ path( "asset-category-chart/", views.asset_category_chart, name="asset-category-chart" ), - path( - "asset-history", - views.asset_history, - name="asset-history", - ), + # path( + # "asset-history", + # views.asset_history, + # name="asset-history", + # ), path( "asset-history-single-view/", views.asset_history_single_view, @@ -188,7 +305,77 @@ urlpatterns = [ views.asset_history_search, name="asset-history-search", ), - path("asset-tab/", views.asset_tab, name="asset-tab"), + path( + "asset-request-allocation-view/", + request_and_allocation.RequestAndAllocationView.as_view(), + name="asset-request-allocation-view", + ), + path( + "list-asset-request-allocation", + request_and_allocation.AllocationList.as_view(), + name="list-asset-request-allocation", + ), + path( + "list-asset", + request_and_allocation.AssetList.as_view(), + name="list-asset", + ), + path( + "tab-asset-request-allocation", + request_and_allocation.RequestAndAllocationTab.as_view(), + name="tab-asset-request-allocation", + ), + path( + "list-asset-request", + request_and_allocation.AssetRequestList.as_view(), + name="list-asset-request", + ), + path( + "list-asset-allocation", + request_and_allocation.AssetAllocationList.as_view(), + name="list-asset-allocation", + ), + path( + "nav-asset-request-allocation", + request_and_allocation.RequestAndAllocationNav.as_view(), + name="nav-asset-request-allocation", + ), + path( + "asset-detail-view//", + request_and_allocation.AssetDetailView.as_view(), + name="asset-detail-view", + ), + path( + "asset-request-detail-view//", + request_and_allocation.AssetRequestDetailView.as_view(), + name="asset-request-detail-view", + ), + path( + "asset-request-tab-list-view//", + asset_tab.AssetRequestTab.as_view(), + name="asset-request-tab-list-view", + ), + path( + "assets-tab-list-view//", + asset_tab.AssetTabListView.as_view(), + name="assets-tab-list-view", + ), + path( + "assets-tab-list-view//", + asset_tab.AssetTabListView.as_view(), + name="assets-tab-list-view", + ), + path( + "asset-allocation-detail-view//", + request_and_allocation.AssetAllocationDetailView.as_view(), + name="asset-allocation-detail-view", + ), + path( + "asset-request-approve-form//", + request_and_allocation.AssetApproveFormView.as_view(), + name="asset-request-approve-form", + ), + path("asset-tab/", views.asset_tab, name="asset-tab"), path( "profile-asset-tab/", views.profile_asset_tab, @@ -199,9 +386,19 @@ urlpatterns = [ views.asset_request_tab, name="asset-request-tab", ), + # path( + # "dashboard-asset-request-approve", + # views.dashboard_asset_request_approve, + # name="dashboard-asset-request-approve", + # ), path( - "main-dashboard-asset-requests", - views.asset_dashboard_requests, - name="main-dashboard-asset-requests", + "dashboard-asset-request-approve", + dashboard.AssetRequestToApprove.as_view(), + name="dashboard-asset-request-approve", + ), + path( + "dashboard-allocated-asset", + dashboard.AllocatedAssetsList.as_view(), + name="dashboard-allocated-asset", ), ] diff --git a/asset/views.py b/asset/views.py index 20d300673..d8e24d278 100644 --- a/asset/views.py +++ b/asset/views.py @@ -315,6 +315,19 @@ def asset_delete(request, asset_id): ) else: asset_del(request, asset) + + if request.GET.get("instance_ids"): + instances_ids = request.GET.get("instance_ids") + instances_list = json.loads(instances_ids) + if asset_id in instances_list: + instances_list.remove(asset_id) + previous_instance, next_instance = closest_numbers( + json.loads(instances_ids), asset_id + ) + return redirect( + f"/asset/asset-information/{next_instance}/?{previous_data}&instance_ids={instances_list}&asset_info=true" + ) + if len(eval_validate(instances_ids)) <= 1: return HttpResponse("") @@ -741,8 +754,7 @@ def asset_allocate_creation(request): if request.method == "POST": form = AssetAllocationForm(request.POST) if form.is_valid(): - asset = form.instance.asset_id.id - asset = Asset.objects.filter(id=asset).first() + asset = form.instance.asset_id asset.asset_status = "In use" asset.save() instance = form.save() @@ -1496,6 +1508,9 @@ def asset_batch_number_delete(request, batch_id): Returns: - message of the return """ + request_copy = request.GET.copy() + request_copy.pop("requests_ids", None) + previous_data = request_copy.urlencode() previous_data = request.GET.urlencode() try: asset_batch_number = AssetLot.objects.get(id=batch_id) @@ -1504,7 +1519,7 @@ def asset_batch_number_delete(request, batch_id): ) if assigned_batch_number: messages.error(request, _("Batch number in-use")) - return redirect(f"/asset/asset-batch-number-search?{previous_data}") + return redirect(f"/asset/asset-batch-list?{previous_data}") asset_batch_number.delete() messages.success(request, _("Batch number deleted")) except AssetLot.DoesNotExist: @@ -1791,7 +1806,7 @@ def asset_history_search(request): @login_required @owner_can_enter("asset.view_asset", Employee) -def asset_tab(request, emp_id): +def asset_tab(request, pk): """ This function is used to view asset tab of an employee in employee individual view. @@ -1802,7 +1817,7 @@ def asset_tab(request, emp_id): Returns: return asset-tab template """ - employee = Employee.objects.get(id=emp_id) + employee = Employee.objects.get(id=pk) assets_requests = employee.requested_employee.all() assets = employee.allocated_employee.all() assets_ids = ( @@ -1812,9 +1827,9 @@ def asset_tab(request, emp_id): "assets": assets, "requests": assets_requests, "assets_ids": assets_ids, - "employee": emp_id, + "employee": pk, } - return render(request, "tabs/asset-tab.html", context=context) + return render(request, "tabs/main_asset_tab.html", context=context) @login_required diff --git a/attendance/cbv/__init__.py b/attendance/cbv/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/attendance/cbv/accessibility.py b/attendance/cbv/accessibility.py new file mode 100644 index 000000000..1db5a8029 --- /dev/null +++ b/attendance/cbv/accessibility.py @@ -0,0 +1,40 @@ +""" +Accessiblility +""" + +from django.contrib.auth.context_processors import PermWrapper +from base.methods import check_manager +from employee.models import Employee + + + +def attendance_accessibility(request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs) -> bool: + """ + permission for attendance tab + """ + + check_manages = check_manager(request.user.employee_get, instance) + employee = Employee.objects.get(id=instance.pk) + if check_manages or request.user.has_perm("attendance.view_attendance") or request.user == employee.employee_user_id: + return True + return False + +def penalty_accessibility(request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs) -> bool: + """ + permission for penalty tab + """ + + employee = Employee.objects.get(id=instance.pk) + check_manages = check_manager(request.user.employee_get,instance) + if request.user.has_perm("attendance.view_penaltyaccount") or request.user == employee.employee_user_id or check_manages: + return True + return False + +def create_attendance_request_accessibility( + request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + employee = Employee.objects.get(id=instance.pk) + if ( request.user == employee.employee_user_id): + return True + return False + diff --git a/attendance/cbv/attendance_activity.py b/attendance/cbv/attendance_activity.py new file mode 100644 index 000000000..e8edd6170 --- /dev/null +++ b/attendance/cbv/attendance_activity.py @@ -0,0 +1,220 @@ +""" +this page is handling the cbv methods of attendance activity page +""" + +from typing import Any +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from attendance.filters import AttendanceActivityFilter +from attendance.forms import AttendanceActivityExportForm +from attendance.models import ( + AttendanceActivity, +) +from base.methods import filtersubordinates, is_reportingmanager +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaListView, + HorillaNavView, + TemplateView, +) +from horilla_views.cbv_methods import login_required + + +@method_decorator(login_required, name="dispatch") +class AttendanceActivityView(TemplateView): + """ + for my attendance page view + """ + + template_name = "cbv/attendance_activity/attendance_activity_home.html" + + +@method_decorator(login_required, name="dispatch") +class AttendanceActivityListView(HorillaListView): + """ + list view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-activity-search") + self.action_method = None + self.view_id = "deleteview" + if self.request.user.has_perm("attendance.delete_attendanceactivity"): + self.action_method = "get_delete_attendance" + + def get_queryset(self): + queryset = super().get_queryset() + self_attendance_activities = queryset.filter( + employee_id__employee_user_id=self.request.user + ) + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendanceovertime" + ) + return queryset | self_attendance_activities + + filter_class = AttendanceActivityFilter + model = AttendanceActivity + records_per_page = 10 + template_name = "cbv/attendance_activity/delete_inherit.html" + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Attendance Date"), "attendance_date"), + (_("In Date"), "clock_in_date"), + (_("Check In"), "clock_in"), + (_("Check Out"), "clock_out"), + (_("Out Date"), "clock_out_date"), + (_("Duration (HH:MM:SS)"), "duration_format"), + + + ] + + row_attrs = """ + {diff_cell} + hx-get='{attendance_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Attendance Date", "attendance_date"), + ("Check In", "clock_in"), + ("In Date", "clock_in_date"), + ("Check Out", "clock_out"), + ("Out Date", "clock_out_date"), + ("Duration (HH:MM:SS)", "duration_format") + ] + + +@method_decorator(login_required, name="dispatch") +class AttendanceActivityNavView(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-activity-search") + + actions = [ + + { + "action": _("Import"), + "attrs": f""" + "id"="activityInfoImport" + data-toggle = "oh-modal-toggle" + data-target = "#objectCreateModal" + hx-target="#objectCreateModalTarget" + hx-get ="{reverse_lazy('attendance-activity-import')}" + style="cursor: pointer;" + """, + }, + { + "action": _("Export"), + "attrs": f""" + data-toggle = "oh-modal-toggle" + data-target = "#genericModal" + hx-target="#genericModalBody" + hx-get ="{reverse_lazy('attendance-bulk-export')}" + style="cursor: pointer;" + """, + } + + ] + + if self.request.user.has_perm("attendance.delete_attendanceactivity"): + actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + deleteAttendanceNav(); + " + data-action ="delete" + style="cursor: pointer; color:red !important" + """, + } + ) + if not self.request.user.has_perm( + "attendance.delete_attendanceactivity" + ) and not is_reportingmanager(self.request): + actions = None + self.actions = actions + + nav_title = _("Attendance Activity") + filter_body_template = "cbv/attendance_activity/filter.html" + filter_instance = AttendanceActivityFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", _("Employee")), + ("attendance_date", _("Attendance Date")), + ("clock_in_date", _("In Date")), + ("clock_out_date", _("Out Date")), + ("shift_day", _("Shift Day")), + ("employee_id__country", _("Country")), + ("employee_id__employee_work_info__reporting_manager_id", _("Reporting Manager")), + ("employee_id__employee_work_info__shift_id", _("Shift")), + ("employee_id__employee_work_info__work_type_id", _("Work Type")), + ("employee_id__employee_work_info__department_id", _("Department")), + ("employee_id__employee_work_info__job_position_id", _("Job Position")), + ("employee_id__employee_work_info__employee_type_id", _("Employement Type")), + ("employee_id__employee_work_info__company_id", _("Company")), + ] + + +@method_decorator(login_required, name="dispatch") +class AttendanceDetailView(HorillaDetailedView): + """ + Detail view of page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-activity-search") + + model = AttendanceActivity + + title = _("Details") + header = { + "title": "employee_id__get_full_name", + "subtitle": "attendance_detail_subtitle", + "avatar": "employee_id__get_avatar", + } + body = [ + (_("Attendance Date"), "attendance_date"), + (_("Day"), "get_status"), + (_("Check In"), "clock_in"), + (_("Check In Date"), "clock_in_date"), + (_("Check Out"), "clock_out"), + (_("Check Out Date"), "clock_out_date"), + (_("Duration"), "duration_format"), + (_("Shift"), "employee_id__employee_work_info__shift_id"), + (_("Work Type"), "employee_id__employee_work_info__work_type_id"), + ] + action_method = "detail_view_delete_attendance" + + +@method_decorator(login_required, name="dispatch") +class AttendanceBulkExport(TemplateView): + """ + for bulk export + """ + + template_name = "cbv/attendance_activity/attendance_export.html" + + def get_context_data(self, **kwargs: Any): + """ + get data for export + """ + attendances = AttendanceActivity.objects.all() + export_form = AttendanceActivityExportForm + export = AttendanceActivityFilter(queryset=attendances) + context = super().get_context_data(**kwargs) + context["export_form"] = export_form + context["export"] = export + return context diff --git a/attendance/cbv/attendance_request.py b/attendance/cbv/attendance_request.py new file mode 100644 index 000000000..64c6a358b --- /dev/null +++ b/attendance/cbv/attendance_request.py @@ -0,0 +1,502 @@ +""" +Attendance requests +""" + +import json +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from attendance.filters import AttendanceFilters +from attendance.forms import ( + AttendanceRequestForm, + BulkAttendanceRequestForm, + NewRequestForm, +) +from attendance.models import Attendance +from attendance.methods.utils import get_employee_last_name +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from employee.models import Employee +from notifications.signals import notify +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaListView, + HorillaNavView, + HorillaTabView, + HorillaFormView, + TemplateView, +) + + +@method_decorator(login_required, name="dispatch") +class AttendancesRequestView(TemplateView): + """ + for attendance request page + """ + + template_name = "cbv/attendance_request/attendance_request.html" + + +@method_decorator(login_required, name="dispatch") +class AttendancesRequestTabView(HorillaTabView): + """ + tabview of attendance request page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendance-container" + self.tabs = [ + { + "title": _("Requested Attendances"), + "url": f"{reverse('attendance-request-list-tab')}", + }, + { + "title": _("All Attendances"), + "url": f"{reverse('attendance-list-tab')}", + }, + ] + + +@method_decorator(login_required, name="dispatch") +class AttendancesRequestListView(HorillaListView): + """ + list view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-request-tab") + + filter_class = AttendanceFilters + model = Attendance + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("In Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Overtime"), "attendance_overtime"), + + ] + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Date", "attendance_date"), + ("In Date", "attendance_clock_in_date"), + ("Out Date", "attendance_clock_out_date"), + ("At Work", "attendance_worked_hour"), + ("Overtime", "attendance_overtime"), + ] + row_status_indications = [ + ( + "bulk-request--dot", + _("Bulk-Requests"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=is_bulk_request]').val('true'); + $('[name=attendance_validated]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "not-validated--dot", + _("Not Validated"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=attendance_validated]').val('false'); + $('[name=is_bulk_request]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "validated--dot", + _("Validated"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=attendance_validated]').val('true'); + $('[name=is_bulk_request]').val('unknown').change(); + $('#applyFilter').click(); + + " + """, + ), + ] + + row_status_class = "validated-{attendance_validated}" + + +@method_decorator(login_required, name="dispatch") +class AttendanceRequestListTab(AttendancesRequestListView): + """ + Attendance request tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendance-requests-container" + + template_name = "cbv/attendance_request/attendance_request_tab.html" + + columns = [ + col for col in AttendancesRequestListView.columns if col[1] != "status_col" + ] + + action_method = "request_actions" + row_attrs = """ + id = "requestedattendanceTr{get_instance_id}" + data-attendance-id="{get_instance_id}" + data-toggle="oh-modal-toggle" + data-target="#validateAttendanceRequest" + hx-get = "{detail_view}?instance_ids={ordered_ids}" + hx-trigger ="click" + hx-target="#validateAttendanceRequestModalBody" + """ + + def get_queryset(self): + queryset = super().get_queryset() + self_data = queryset + queryset = queryset.filter( + is_validate_request=True, + ) + queryset = filtersubordinates( + request=self.request, + perm="attendance.view_attendance", + queryset=queryset, + ) + queryset = queryset | self_data.filter( + employee_id__employee_user_id=self.request.user, + is_validate_request=True, + ) + return queryset + + +@method_decorator(login_required, name="dispatch") +class AttendanceListTab(AttendancesRequestListView): + """ + Attendance tab + """ + + def get_queryset(self): + queryset = super().get_queryset() + data = queryset + attendances = filtersubordinates( + request=self.request, + perm="attendance.view_attendance", + queryset=queryset, + ) + queryset = attendances | data.filter( + employee_id__employee_user_id=self.request.user + ) + queryset = queryset.filter( + employee_id__is_active=True, + ) + return queryset + + actions = [ + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{change_attendance}" + hx-target="#genericModalBody" + + """, + } + ] + + row_attrs = """ + {diff_cell} + id = "allattendanceTr{get_instance_id}" + hx-get='{attendance_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + hx-trigger ="click" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +class AttendanceRequestNav(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-request-tab") + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('request-new-attendance')}" + hx-target="#genericModalBody" + """ + if self.request.user.has_perm( + "attendance.add_attendanceovertime" + ) or is_reportingmanager(self.request): + + self.actions = [ + { + "action": _("Bulk Approve"), + "attrs": """ + onclick=" + reqAttendanceBulkApprove(); + " + style="cursor: pointer;" + """, + }, + { + "action": _("Bulk Reject"), + "attrs": """ + onclick="reqAttendanceBulkReject();" + style="color:red !important" + """, + }, + ] + else: + self.actions = None + + nav_title = _("Attendances") + filter_body_template = "cbv/attendances/attendances_filter_page.html" + filter_instance = AttendanceFilters() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", "Employee"), + ("attendance_date", "Attendance Date"), + ("attendance_clock_in_date", "In Date"), + ("attendance_clock_out_date", "Out Date"), + ("employee_id__country", "Country"), + ("employee_id__employee_work_info__reporting_manager_id", "Reporting Manager"), + ("shift_id", "Shift"), + ("work_type_id", "Work Type"), + ("minimum_hour", " Min Hour"), + ("employee_id__employee_work_info__department_id", "Department"), + ("employee_id__employee_work_info__job_position_id", "Job Position"), + ("employee_id__employee_work_info__employee_type_id", "Employement Type"), + ("employee_id__employee_work_info__company_id", "Company"), + ] + + +@method_decorator(login_required, name="dispatch") +class AttendanceListTabDetailView(HorillaDetailedView): + """ + Detail view of page + """ + + model = Attendance + + title = _("Details") + header = { + "title": "employee_id__get_full_name", + "subtitle": "attendances_detail_subtitle", + "avatar": "employee_id__get_avatar", + } + body = [ + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("Check In Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Check Out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Overtime"), "attendance_overtime"), + (_("Activities"), "attendance_detail_activity_col", True), + ] + + actions = [ + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + onclick="event.stopPropagation();" + class="oh-btn oh-btn--info w-100" + data-toggle="oh-modal-toggle" + data-target="#genericModalEdit" + hx-get="{change_attendance}?all_attendance=true" + hx-target="#genericModalEditBody" + + """, + } + ] + + +class NewAttendanceRequestFormView(HorillaFormView): + """ + form view for create attendance request + """ + + form_class = NewRequestForm + model = Attendance + new_display_title = _("New Attendance Request") + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendanceRequest" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form = choosesubordinates( + self.request, self.form, "attendance.change_attendance" + ) + self.form.fields["employee_id"].queryset = self.form.fields[ + "employee_id" + ].queryset | Employee.objects.filter(employee_user_id=self.request.user) + self.form.fields["employee_id"].initial = self.request.user.employee_get.id + if self.request.GET.get("emp_id"): + emp_id = self.request.GET.get("emp_id") + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=emp_id + ) + self.form.fields["employee_id"].initial = emp_id + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Attendance Request") + return context + + def form_valid(self, form: NewRequestForm) -> HttpResponse: + if form.is_valid(): + message = _("New Attendance request created") + if form.new_instance is not None: + form.new_instance.save() + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +class BulkAttendanceRequestFormView(HorillaFormView): + """ + form view for create bulk attendance request + """ + + form_class = BulkAttendanceRequestForm + model = Attendance + new_display_title = _("New Attendance Request") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form = choosesubordinates( + self.request, self.form, "attendance.change_attendance" + ) + self.form.fields["employee_id"].queryset = self.form.fields[ + "employee_id" + ].queryset | Employee.objects.filter(employee_user_id=self.request.user) + self.form.fields["employee_id"].initial = self.request.user.employee_get.id + return context + + def form_valid(self, form: BulkAttendanceRequestForm) -> HttpResponse: + form.instance.attendance_clock_in_date = self.request.POST.get("from_date") + form.instance.attendance_date = self.request.POST.get("from_date") + if form.is_valid(): + if form.instance.pk: + message = _("New Attendance request updated") + else: + message = _("New Attendance request created") + instance = form.save(commit=False) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +class UpdateAttendanceRequestFormView(HorillaFormView): + """ + form view for update attendance request + """ + + form_class = AttendanceRequestForm + model = Attendance + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendanceRequest" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Attendance Request") + return context + + def form_valid(self, form: AttendanceRequestForm) -> HttpResponse: + if form.is_valid(): + attendance = Attendance.objects.get(id=self.form.instance.pk) + instance = form.save() + instance.employee_id = attendance.employee_id + instance.id = attendance.id + if attendance.request_type != "create_request": + attendance.requested_data = json.dumps(instance.serialize()) + attendance.request_description = instance.request_description + # set the user level validation here + attendance.is_validate_request = True + attendance.save() + else: + instance.is_validate_request_approved = False + instance.is_validate_request = True + instance.save() + messages.success(self.request, _("Attendance update request created.")) + employee = attendance.employee_id + if attendance.employee_id.employee_work_info.reporting_manager_id: + reporting_manager = ( + attendance.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ) + user_last_name = get_employee_last_name(attendance) + notify.send( + self.request.user, + recipient=reporting_manager, + verb=f"{employee.employee_first_name} {user_last_name}'s\ + attendance update request for {attendance.attendance_date} is created", + verb_ar=f"تم إنشاء طلب تحديث الحضور لـ {employee.employee_first_name} \ + {user_last_name }في {attendance.attendance_date}", + verb_de=f"Die Anfrage zur Aktualisierung der Anwesenheit von \ + {employee.employee_first_name} {user_last_name} \ + für den {attendance.attendance_date} wurde erstellt", + verb_es=f"Se ha creado la solicitud de actualización de asistencia para {employee.employee_first_name}\ + {user_last_name} el {attendance.attendance_date}", + verb_fr=f"La demande de mise à jour de présence de {employee.employee_first_name}\ + {user_last_name} pour le {attendance.attendance_date} a été créée", + redirect=reverse("request-attendance-view") + + f"?id={attendance.id}", + icon="checkmark-circle-outline", + ) + detail_view = self.request.GET.get("detail_view") + all_attendance = self.request.GET.get("all_attendance") + if detail_view == "true": + return HttpResponse( + f""" + """ + ) + elif all_attendance == "true": + return HttpResponse( + f""" + """ + ) + + return self.HttpResponse() + return super().form_valid(form) diff --git a/attendance/cbv/attendance_tab.py b/attendance/cbv/attendance_tab.py new file mode 100644 index 000000000..80a213aa5 --- /dev/null +++ b/attendance/cbv/attendance_tab.py @@ -0,0 +1,124 @@ +""" +This page is handling the cbv methods of work type and shift tab in employee profile page. +""" + +import json +from typing import Any +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from attendance.cbv.attendance_request import AttendanceRequestListTab +from attendance.cbv.hour_account import HourAccountList +from attendance.cbv.my_attendances import MyAttendancesListView +from attendance.filters import AttendanceFilters +from attendance.models import Attendance +from base.methods import filtersubordinates +from base.request_and_approve import paginator_qry +from employee.models import Employee +from horilla_views.generic.cbv.views import HorillaListView, HorillaTabView + +class AttendanceTabView(HorillaTabView): + """ + generic tab view for attendance + """ + + # template_name = "cbv/work_shift_tab/extended_work-shift.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendance-container" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + context["emp_id"] = pk + employee = Employee.objects.get(id=pk) + context["instance"] = employee + context["tabs"] = [ + { + "title": _("Requested Attendances"), + "url": f"{reverse('attendance-request-individual-tab',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Create Attendance Request", + "accessibility": "attendance.cbv.accessibility.create_attendance_request_accessibility", + "attrs": f""" + hx-get="{reverse('request-new-attendance')}?emp_id={pk}", + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ], + }, + { + "title": _("Validate Attendances"), + "url": f"{reverse('validate-attendance-individual-tab',kwargs={'pk': pk})}", + }, + { + "title": _("Hour Account"), + "url": f"{reverse('attendance-overtime-individual-tab',kwargs={'pk': pk})}", + }, + { + "title": _("All Attendances"), + "url": f"{reverse('all-attendances-individual-tab',kwargs={'pk': pk})}", + }, + ] + return context + + +class RequestedAttendanceIndividualView(AttendanceRequestListTab): + """ + list view for requested attendance tab view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("attendance-request-individual-tab",kwargs= {'pk': pk} ) + self.view_id = "attendance-requests-container" + + def get_queryset(self): + queryset = HorillaListView.get_queryset(self) + pk = self.request.resolver_match.kwargs.get('pk') + queryset = queryset.filter( + employee_id__employee_user_id=pk, + is_validate_request=True, + ) + return queryset + + +class HourAccountIndividualTabView(HourAccountList): + """ + list view for hour account tab + """ + + template_name = "cbv/hour_account/hour_account_main.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("attendance-overtime-individual-tab",kwargs= {'pk': pk} ) + self.view_id = "ot-table" + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset + + +class AllAttendancesList(MyAttendancesListView): + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + context["search_url"] = ( + f"{reverse('all-attendances-individual-tab',kwargs={'pk': pk})}" + ) + return context + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset diff --git a/attendance/cbv/attendances.py b/attendance/cbv/attendances.py new file mode 100644 index 000000000..010051d11 --- /dev/null +++ b/attendance/cbv/attendances.py @@ -0,0 +1,731 @@ +""" +this page is handling the cbv methods of attendances page +""" + +import datetime +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import resolve, reverse, reverse_lazy +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +import django_filters +from attendance.cbv.attendance_activity import AttendanceActivityListView +from attendance.filters import AttendanceFilters +from attendance.forms import AttendanceExportForm, AttendanceForm, AttendanceUpdateForm +from attendance.cbv.attendance_tab import AttendanceTabView +from attendance.models import ( + Attendance, + AttendanceValidationCondition, + strtime_seconds, +) +from base.decorators import manager_can_enter +from base.filters import PenaltyFilter +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from base.models import PenaltyAccounts +from employee.cbv.employee_profile import EmployeeProfileView +from employee.cbv.employees import EmployeeNav, EmployeeCard, EmployeesList +from employee.filters import EmployeeFilter +from employee.models import Employee +from horilla.filters import HorillaFilterSet +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaListView, + HorillaNavView, + HorillaTabView, + TemplateView, + HorillaFormView, +) +from horilla_views.cbv_methods import login_required, render_template + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class AttendancesView(TemplateView): + """ + for attendances page + """ + + template_name = "cbv/attendances/attendance_view_page.html" + + +class AttendancesListView(HorillaListView): + """ + list view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendances-list-view") + if self.request.user.has_perm( + "attendance.change_attendance" + ) or is_reportingmanager(self.request): + self.option_method = "attendance_actions" + + filter_class = AttendanceFilters + model = Attendance + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("In Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Pending Hour"), "hours_pending"), + (_("Overtime"), "attendance_overtime"), + ] + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Date", "attendance_date"), + ("Day", "attendance_day__day"), + ("Check-In", "attendance_clock_in"), + ("In Date", "attendance_clock_in_date"), + ("Check-Out", "attendance_clock_out"), + ("Out Date", "attendance_clock_out_date"), + ("Shift", "shift_id__employee_shift"), + ("Work Type", "work_type_id__work_type"), + ("Min Hour", "minimum_hour"), + ("At Work", "attendance_worked_hour"), + ("Pending Hour", "hours_pending"), + ("Overtime", "attendance_overtime"), + ] + records_per_page = 10 + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class AttendancesTabView(HorillaTabView): + """ + tabview of candidate page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "attendances-tab" + self.tabs = [ + { + "title": _("Attendance To Validate"), + "url": f"{reverse('validate-attendance-tab')}", + "actions": [ + { + "action": "Validate", + "attrs": """ + onclick=" + bulkValidateTabAttendance(); + " + style="cursor: pointer;" + """, + } + ], + }, + { + "title": _(" OT Attendances"), + "url": f"{reverse('ot-attendance-tab')}", + "actions": [ + { + "action": "Approve OT", + "attrs": """ + onclick=" + otBulkValidateTabAttendance(); + " + style="cursor: pointer;" + """, + } + ], + }, + { + "title": _(" Validated Attendances"), + "url": f"{reverse('validated-attendance-tab')}", + }, + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class AttendancesNavView(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendances-tab-view") + self.search_in = [ + ("attendance_day", _("Day")), + ("shift_id", _("Shift")), + ("work_type_id", _("Work Type")), + ("employee_id__employee_work_info__department_id", _("Department")), + ( + "employee_id__employee_work_info__job_position_id", + _("Job Position"), + ), + ("employee_id__employee_work_info__company_id", _("Company")), + ] + self.create_attrs = f""" + hx-get="{reverse_lazy("attendance-create")}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + actions = [ + { + "action": _("Import"), + "attrs": """ + onclick=" + importAttendanceNav(); + " + data-toggle = "oh-modal-toggle" + data-target = "#attendanceImport + " + style="cursor: pointer;" + """, + }, + { + "action": _("Export"), + "attrs": f""" + data-toggle = "oh-modal-toggle" + data-target = "#genericModal" + hx-target="#genericModalBody" + hx-get ="{reverse('attendences-navbar-export')}" + style="cursor: pointer;" + """, + }, + ] + if self.request.user.has_perm("attendance.add_attendance"): + actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + bulkDeleteAttendanceNav(); + " + data-action="delete" + style="cursor: pointer; color:red !important" + """, + } + ) + self.actions = actions + + nav_title = _("Attendances") + filter_body_template = "cbv/attendances/attendances_filter_page.html" + filter_instance = AttendanceFilters() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", _("Employee")), + ("attendance_date", _("Attendance Date")), + ("shift_id", _("Shift")), + ("Work Type", _("work_type_id")), + ("minimum_hour", _("Min Hour")), + ("employee_id__country", "Country"), + ( + "employee_id__employee_work_info__reporting_manager_id", + _("Reporting Manager"), + ), + ("employee_id__employee_work_info__department_id", _("Department")), + ("employee_id__employee_work_info__job_position_id", _("Job Position")), + ( + "employee_id__employee_work_info__employee_type_id", + _("Employement Type"), + ), + ("employee_id__employee_work_info__company_id", _("Company")), + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class AttendancesExportNav(TemplateView): + """ + for bulk export + """ + + template_name = "cbv/attendances/attendances_export_page.html" + + def get_context_data(self, **kwargs: Any): + """ + get data for export + """ + + attendances = Attendance.objects.all() + export_form = AttendanceExportForm + export = AttendanceFilters(queryset=attendances) + context = super().get_context_data(**kwargs) + context["export_form"] = export_form + context["export"] = export + return context + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class ValidateAttendancesList(AttendancesListView): + """ + validate tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("validate-attendance-tab") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter( + attendance_validated=False, employee_id__is_active=True + ) + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendance" + ) + return queryset + + selected_instances_key_id = "validateselectedInstances" + action_method = "validate_button" + row_attrs = """ + hx-get='{validate_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + header_attrs = { + "action": """ + style="width:150px !important;" + """ + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class OTAttendancesList(AttendancesListView): + """ + OT tab + """ + + selected_instances_key_id = "overtimeselectedInstances" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("ot-attendance-tab") + self.action_method = "ot_approve" + + def get_queryset(self): + queryset = super().get_queryset() + minot = strtime_seconds("00:30") + condition = ( + AttendanceValidationCondition.objects.first() + ) # and condition.minimum_overtime_to_approve is not None + if condition is not None: + minot = strtime_seconds(condition.minimum_overtime_to_approve) + queryset = queryset.filter( + overtime_second__gt=0, + attendance_validated=True, + ) + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendance" + ) + return queryset + + row_attrs = """ + hx-get='{ot_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + header_attrs = { + "action": """ + style="width:150px !important;" + """ + } + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class ValidatedAttendancesList(AttendancesListView): + """ + validated tab + """ + + selected_instances_key_id = "validatedselectedInstances" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("validated-attendance-tab") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter( + attendance_validated=True, employee_id__is_active=True + ) + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendance" + ) + return queryset + + row_attrs = """ + hx-get='{validated_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class GenericAttendancesDetailView(HorillaDetailedView): + """ + Generic Detail view of page + """ + + model = Attendance + + title = _("Details") + header = { + "title": "employee_id__get_full_name", + "subtitle": "attendances_detail_subtitle", + "avatar": "employee_id__get_avatar", + } + body = [ + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("Check In Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Check Out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Overtime"), "attendance_overtime"), + (_("Activities"), "attendance_detail_activity_col", True), + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class ValidateDetailView(GenericAttendancesDetailView): + """ + detail view for validate tab + """ + + action_method = "validate_detail_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class OtDetailView(GenericAttendancesDetailView): + """ + detail view for OT tab + """ + + action_method = "ot_detail_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.view_attendance"), name="dispatch") +class ValidatedDetailView(GenericAttendancesDetailView): + """ + detail view for validate tab + """ + + action_method = "validated_detail_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.add_attendance"), name="dispatch") +class AttendancesFormView(HorillaFormView): + """ + form view + """ + + form_class = AttendanceForm + model = Attendance + new_display_title = _("Add Attendances") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form = choosesubordinates( + self.request, self.form, "attendance.add_attendance" + ) + + context["form"] = self.form + context["view_id"] = "attendanceCreate" + + return context + + def form_valid(self, form: AttendanceForm) -> HttpResponse: + if form.is_valid(): + message = _("Attendance Added") + form.save() + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.change__attendance"), name="dispatch") +class AttendanceUpdateFormView(HorillaFormView): + """ + form for update + """ + + model = Attendance + form_class = AttendanceUpdateForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Edit Attendance") + + context["view_id"] = "attendanceUpdate" + + return context + + def form_valid(self, form: AttendanceUpdateForm) -> HttpResponse: + if form.is_valid(): + message = _("Attandance Updated") + form.save() + messages.success(self.request, message) + return HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class AttendanceDetailActivityList(AttendanceActivityListView): + """ + List view for activity col in detail view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.action_method = None + resolved = resolve(self.request.path_info) + kwargs = { + "pk": resolved.kwargs.get("pk"), + } + self.search_url = reverse("get-attendance-activities", kwargs=kwargs) + + bulk_select_option = None + row_attrs = "" + + def get_queryset(self): + queryset = super().get_queryset() + pk = Attendance.find(self.kwargs.get("pk")) + queryset = queryset.filter( + attendance_date=pk.attendance_date, employee_id=pk.employee_id + ) + + return queryset + + +class PenaltyAccountListView(HorillaListView): + """ + list view for penalty tab + """ + + filter_class = PenaltyFilter + model = PenaltyAccounts + records_per_page = 3 + columns = [ + (_("Leave Type"), "leave_type_id"), + (_("Minus Days"), "minus_leaves"), + (_("Deducted From CFD"), "get_deduct_from_carry_forward"), + (_("Penalty amount"), "penalty_amount"), + (_("Created Date"), "created_at"), + (_("Penalty Type"),"penalty_type_col") + ] + + actions = [ + + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs" : """ + class="oh-btn oh-btn--light-bkg w-100 text-danger" + hx-confirm="Are you sure you want to delete this penalty?" + hx-post="{get_delete_url}" + hx-target="#penaltyTr{get_delete_instance}" + hx-swap="delete" + + """ + } + + ] + + row_attrs = """ + id = "penaltyTr{get_delete_instance}" + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get("pk") + self.search_url = reverse("individual-panlty-list-view", kwargs={"pk": pk}) + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset + + +class ValidateAttendancesIndividualTabView(AttendancesListView): + """ + list view for validate attendance tab view + """ + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter( + employee_id=pk, + attendance_validated=False, + employee_id__is_active=True, + ) + queryset = ( + filtersubordinates(self.request, queryset, "attendance.view_attendance") + | queryset + ) + return queryset + + selected_instances_key_id = "validateselectedInstances" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get("pk") + self.search_url = reverse( + "validate-attendance-individual-tab", kwargs={"pk": pk} + ) + if self.request.user.has_perm( + "attendance.change_attendance" + ) or is_reportingmanager(self.request): + self.action_method = "validate_button" + self.view_id = "validate-container" + + row_attrs = """ + hx-get='{individual_validate_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +class ValidateAttendancesIndividualDetailView(GenericAttendancesDetailView): + """ + Validate tab detail view in single view of employee + """ + + action_method = "validate_detail_actions" + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + obj = queryset.get(pk=pk) + employee_id = obj.employee_id + if is_reportingmanager(self.request): + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendance" + ) | queryset.filter(employee_id=self.request.user.employee_get) + elif self.request.user.has_perm("attendance.view_attendance"): + queryset = queryset.filter(employee_id=employee_id) + else: + queryset = queryset.filter(employee_id=self.request.user.employee_get) + return queryset + + @method_decorator(login_required, name="dispatch") + def dispatch(self, *args, **kwargs): + return super(GenericAttendancesDetailView, self).dispatch(*args, **kwargs) + + +EmployeeProfileView.add_tab( + tabs=[ + { + "title": "Attendance", + # "view": views.attendance_tab, + "view": AttendanceTabView.as_view(), + "accessibility": "attendance.cbv.accessibility.attendance_accessibility", + }, + { + "title": "Penalty Account", + "view": PenaltyAccountListView.as_view(), + "accessibility": "attendance.cbv.accessibility.penalty_accessibility", + }, + ] +) + + +def get_working_today(queryset, _name, value): + today = datetime.datetime.now().date() + yesterday = today - datetime.timedelta(days=1) + + working_employees = Attendance.objects.filter( + attendance_date__gte=yesterday, + attendance_date__lte=today, + attendance_clock_out_date__isnull=True, + ).values_list("employee_id", flat=True) + + if value: + queryset = queryset.filter(id__in=working_employees) + else: + queryset = queryset.exclude(id__in=working_employees) + return queryset + + +og_init = EmployeeFilter.__init__ + + +def online_init(self, *args, **kwargs): + og_init(self, *args, **kwargs) + custom_field = django_filters.BooleanFilter( + label="Working", method=get_working_today + ) + self.filters["working_today"] = custom_field + self.form.fields["working_today"] = custom_field.field + self.form.fields["working_today"].widget.attrs.update( + { + "class": "oh-select oh-select-2 w-100", + } + ) + + +status_indications = [ + ( + "offline--dot", + _("Offline"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=working_today]').val('false'); + $('#applyFilter').click(); + " + """, + ), + ( + "online--dot", + _("Online"), + """ + onclick="$('#applyFilter').closest('form').find('[name=working_today]').val('true'); + $('#applyFilter').click(); + " + """, + ), +] + + +def offline_online(self): + """ + This method for get custome coloumn for rating. + """ + + return render_template( + path="cbv/employees_view/offline_online.html", + context={"instance": self}, + ) + + +EmployeeFilter.__init__ = online_init +EmployeeNav.filter_instance = EmployeeFilter() +EmployeeCard.card_status_indications = status_indications +EmployeesList.row_status_indications = status_indications +Employee.offline_online = offline_online diff --git a/attendance/cbv/break_point.py b/attendance/cbv/break_point.py new file mode 100644 index 000000000..9213fe4b6 --- /dev/null +++ b/attendance/cbv/break_point.py @@ -0,0 +1,120 @@ +""" +this page is handling the cbv methods for Break point conditions in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from attendance.filters import AttendanceBreakpointFilter +from attendance.forms import AttendanceValidationConditionForm +from attendance.models import AttendanceValidationCondition +from horilla.decorators import permission_required +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class BreakPointList(HorillaListView): + """ + list view of the Break point conditions in settings + """ + + model = AttendanceValidationCondition + filter_class = AttendanceBreakpointFilter + + columns = [ + (_("Auto Validate Till"), "validation_at_work"), + (_("Min Hour To Approve OT"), "minimum_overtime_to_approve"), + (_("OT Cut-Off/Day"), "overtime_cutoff"), + (_("Actions"), "break_point_actions"), + ] + header_attrs = { + "validation_at_work": """ style="width:200px !important" """, + + } + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class BreakPointNavView(HorillaNavView): + """ + navbar of attendance breakpoint view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + condition = AttendanceValidationCondition.objects.first() + if not condition and self.request.user.has_perm("attendance.add_attendancevalidationcondition"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('break-point-create-form')}" + """ + + nav_title = _("Break Point Condition") + search_swap_target = "#listContainer" + filter_instance = AttendanceBreakpointFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class BreakPointCreateForm(HorillaFormView): + """ + form view for create and edit Break Point in settings + """ + + model = AttendanceValidationCondition + form_class = AttendanceValidationConditionForm + new_display_title = _("Create Attendance condition") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # form = self.form_class() + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Attendance condition") + context[form] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Attendance condition") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: AttendanceValidationConditionForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + messages.success( + self.request, _("Attendance Break-point settings updated.") + ) + else: + messages.success( + self.request, _("Attendance Break-point settings created.") + ) + form.save() + return self.HttpResponse("") + return super().form_valid(form) diff --git a/attendance/cbv/check_in_check_out.py b/attendance/cbv/check_in_check_out.py new file mode 100644 index 000000000..6423ce7af --- /dev/null +++ b/attendance/cbv/check_in_check_out.py @@ -0,0 +1,50 @@ +from typing import Any + +from django.urls import reverse +from attendance.filters import AttendanceGeneralSettingFilter +from attendance.models import AttendanceGeneralSetting +from horilla_views.generic.cbv.views import HorillaListView,HorillaNavView +from django.utils.translation import gettext_lazy as _ + +class CheckInCheckOutListView(HorillaListView): + """ + List view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "check-in-check-out" + + model = AttendanceGeneralSetting + filter_class = AttendanceGeneralSettingFilter + + columns = [ + (_("Company"), "company_col"), + (_("Check in/Check out"), "check_in_check_out_col"), + ] + + header_attrs = { + "company_col" : """ + style = "width:100px !important" + """, + } + + bulk_select_option = False + + +class CheckInCheckOutNavBar(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + # self.search_url = reverse("check-in-check-out-list") + + + nav_title = _("Enable Check In/Check out") + filter_instance = AttendanceGeneralSettingFilter() + search_swap_target = "#listContainer" + + + \ No newline at end of file diff --git a/attendance/cbv/dashboard.py b/attendance/cbv/dashboard.py new file mode 100644 index 000000000..ea68397bc --- /dev/null +++ b/attendance/cbv/dashboard.py @@ -0,0 +1,137 @@ +""" +this page handles the cbv methods for dashboard +""" + +from datetime import datetime +from typing import Any +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from attendance.cbv.attendances import OTAttendancesList, ValidateAttendancesList +from attendance.filters import LateComeEarlyOutFilter +from attendance.methods.utils import strtime_seconds +from attendance.models import AttendanceLateComeEarlyOut, AttendanceValidationCondition +from base.methods import filtersubordinates +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import HorillaListView + + + +@method_decorator(login_required, name="dispatch") +class DashboardAttendanceToValidate(ValidateAttendancesList): + """ + list view for attendance to validate in dashboard + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-attendance-validate") + self.option_method = "" + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Worked Hours"), "attendance_worked_hour"), + + ] + + header_attrs = { + + "attendance_worked_hour": """ + style="width:100px !important;" + """, + "employee_id": """ + style="width:100px !important;" + """, + "action": """ + style="width:100px !important;" + """, + + } + + records_per_page = 3 + bulk_select_option = False + show_toggle_form = False + + +@method_decorator(login_required, name="dispatch") +class DashboardaAttendanceOT(OTAttendancesList): + """ + list view for OT attendance to validate in dashboard + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-overtime-approve") + self.option_method = "" + + def get_queryset(self): + queryset = super().get_queryset() + condition = AttendanceValidationCondition.objects.first() + minot = strtime_seconds("00:00") + if condition is not None and condition.minimum_overtime_to_approve is not None: + minot = strtime_seconds(condition.minimum_overtime_to_approve) + queryset = queryset.filter( + overtime_second__gte=minot, + attendance_validated=True, + employee_id__is_active=True, + attendance_overtime_approve=False, + ) + queryset = filtersubordinates( + self.request, queryset, "attendance.view_attendance" + ) + return queryset + + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Overtime"), "attendance_overtime"), + ] + header_attrs = { + "action": """ + style="width:100px !important;" + """, + "attendance_overtime": """ + style="width:100px !important;" + """, + "employee_id": """ + style="width:100px !important;" + """, + } + + show_toggle_form = False + records_per_page = 3 + bulk_select_option = False + + + + +@method_decorator(login_required, name="dispatch") +class DashboardOnBreak(HorillaListView): + """ + view for on break employee list + """ + + model = AttendanceLateComeEarlyOut + filter_class = LateComeEarlyOutFilter + show_toggle_form =False + + bulk_select_option = False + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-on-break") + + def get_queryset(self): + queryset = super().get_queryset() + today = datetime.today() + queryset = queryset.filter( + type="early_out", attendance_id__attendance_date=today + ) + return queryset + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + ] + + + \ No newline at end of file diff --git a/attendance/cbv/dashboard_offline_online.py b/attendance/cbv/dashboard_offline_online.py new file mode 100644 index 000000000..57e32b041 --- /dev/null +++ b/attendance/cbv/dashboard_offline_online.py @@ -0,0 +1,97 @@ +""" +this page handles the cbv methods for online and offline employee list in dashboard +""" + +from datetime import date +from typing import Any +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from attendance.cbv.attendances import OTAttendancesList, ValidateAttendancesList + +from base.decorators import manager_can_enter +from employee.filters import EmployeeFilter +from employee.models import Employee +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import HorillaListView + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("leave.view_leaverequest"), name="dispatch") +class DashboardOfflineEmployees(HorillaListView): + """ + list view for offline employees in dashboard + """ + + model = Employee + filter_class = EmployeeFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("not-in-yet") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = ( + EmployeeFilter({"not_in_yet": date.today()}) + .qs.exclude(employee_work_info__isnull=True) + .filter(is_active=True) + ) + + return queryset + + columns = [ + ("Employee", "get_full_name", "get_avatar"), + ("Work Status", "get_leave_status"), + ("Actions", "send_mail_button"), + ] + header_attrs = { + "get_full_name": """ + style="width:200px !important;" + """, + "send_mail_button": """ + style="width:80px !important;" + """, + } + records_per_page = 7 + show_toggle_form = False + bulk_select_option = False + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("leave.view_leaverequest"), name="dispatch") +class DashboardOnlineEmployees(HorillaListView): + """ + list view for online employees in dashboard + """ + + model = Employee + filter_class = EmployeeFilter + show_toggle_form = False + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("not-out-yet") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = ( + EmployeeFilter({"not_out_yet": date.today()}) + .qs.exclude(employee_work_info__isnull=True) + .filter(is_active=True) + ) + + return queryset + + columns = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Work Status", "get_custom_forecasted_info_col"), + ] + + header_attrs = { + "employee_id__get_full_name": """ style="width:200px !important" """, + "get_custom_forecasted_info_col": """ style="width:180px !important" """, + } + + records_per_page = 8 + bulk_select_option = False diff --git a/attendance/cbv/grace_time.py b/attendance/cbv/grace_time.py new file mode 100644 index 000000000..e4f601069 --- /dev/null +++ b/attendance/cbv/grace_time.py @@ -0,0 +1,207 @@ +""" +This page handles grace time in settings page. +""" +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.urls import reverse +from django.contrib import messages +from attendance.filters import GraceTimeFilter +from attendance.forms import GraceTimeForm +from attendance.models import GraceTime +from base.cbv.employee_shift import EmployeeShiftListView +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class GenericGraceTimeListView(HorillaListView): + """ + List view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "all-container" + + model = GraceTime + filter_class = GraceTimeFilter + + columns = [ + (_("Allowed Time"), "allowed_time_col"), + (_("Is active"), "is_active_col"), + (_("Applicable on clock-in"), "applicable_on_clock_in_col"), + (_("Applicable on clock-out"), "applicable_on_clock_out_col"), + (_("Assigned Shifts"), "get_shifts_display"), + + ] + + header_attrs = { + "allowed_time_col" : """ + style = "width:200px !important" + """, + + } + + row_attrs = """ + id = "graceTimeTr{get_instance_id}" + """ + + action_method = "action_col" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class DefaultGraceTimeList(GenericGraceTimeListView): + """ + List of default grace time + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "default-container" + + selected_instances_key_id = "selectedInstancesDefault" + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(is_default=True) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class GraceTimeList(GenericGraceTimeListView): + """ + List of grace time + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "gracetime-container" + + selected_instances_key_id = "selectedInstancesData" + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.exclude(is_default=True) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class DefaultGraceTimeNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + # self.search_url = reverse("grace-time-list") + default_grace_time = GraceTime.objects.filter(is_default=True).first() + if not default_grace_time and self.request.user.has_perm("attendance.add_gracetime"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('grace-time-create')}?default=True" + """ + + nav_title = _("Default Grace Time") + filter_instance = GraceTimeFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="attendance.view_attendancevalidationcondition"), + name="dispatch", +) +class GraceTimeNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # self.search_url = reverse("grace-time-list") + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('grace-time-create')}?default=False" + """ + + nav_title = _("Grace Time") + filter_instance = GraceTimeFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="attendance.add_gracetime"), name="dispatch") +class GraceTimeFormView(HorillaFormView): + """ + Create and edit form + """ + + model = GraceTime + form_class = GraceTimeForm + new_display_title = _("Create grace time") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + is_default = eval(self.request.GET.get("default")) + self.form.fields["is_default"].initial = is_default + if self.form.instance.pk: + self.form.fields["shifts"].initial=self.form.instance.employee_shift.all() + self.form_class.verbose_name = _("Update grace time") + return context + + def form_valid(self, form: GraceTimeForm) -> HttpResponse: + if form.is_valid(): + gracetime = form.save() + if form.instance.pk: + gracetime.employee_shift.clear() + message = _("Grace time updated successfully.") + messages.success(self.request, message) + else: + + message = _("Grace time created successfully.") + messages.success(self.request, message) + shifts = form.cleaned_data.get('shifts') + for shift in shifts: + shift.grace_time_id = gracetime + shift.save() + # form.save() + defaultValue = self.request.GET.get("default") + if defaultValue == "False": + return HttpResponse("") + return HttpResponse("") + return super().form_valid(form) + + + +EmployeeShiftListView.columns.append((_("Grace Time"), "get_grace_time")) +EmployeeShiftListView.sortby_mapping.append(("Grace Time", "grace_time_id")) +EmployeeShiftListView.bulk_update_fields.append("grace_time_id") + diff --git a/attendance/cbv/hour_account.py b/attendance/cbv/hour_account.py new file mode 100644 index 000000000..e644d0bc9 --- /dev/null +++ b/attendance/cbv/hour_account.py @@ -0,0 +1,261 @@ +""" +Hour account page +""" + +from typing import Any +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from attendance.filters import AttendanceOverTimeFilter +from attendance.forms import AttendanceOverTimeExportForm, AttendanceOverTimeForm +from attendance.models import AttendanceOverTime +from base.decorators import manager_can_enter +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) + + +@method_decorator(login_required, name="dispatch") +class HourAccount(TemplateView): + """ + Hour Account + """ + + template_name = "cbv/hour_account/hour_account.html" + +@method_decorator(login_required, name="dispatch") +class HourAccountList(HorillaListView): + """ + List view + """ + + model = AttendanceOverTime + filter_class = AttendanceOverTimeFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-ot-search") + self.view_id = "ot-table" + + if self.request.user.has_perm( + "attendance.add_attendanceovertime" + ): + self.action_method = "hour_actions" + + def get_queryset(self): + queryset = super().get_queryset() + data = queryset + queryset = queryset.filter(employee_id__employee_user_id=self.request.user) + accounts = filtersubordinates( + self.request, data, "attendance.view_attendanceovertime" + ) + return queryset | accounts + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Month"), "get_month_capitalized"), + (_("Year"), "year"), + (_("Worked Hours"), "worked_hours"), + (_("Hours to Validate"), "not_validated_hrs"), + (_("Pending Hours"), "pending_hours"), + (_("Overtime Hours"), "overtime"), + (_("Not Approved OT Hours"), "not_approved_ot_hrs"), + ] + + header_attrs = { + "employee_id" : """ + style='width:200px !important' + """, + "not_approved_ot_hrs" : """ + style='width:180px !important' + """, + "action" : """ + style="width:160px !important" + """ + } + + row_attrs = """ + hx-get='{hour_account_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Month", "get_month_capitalized"), + ("Year", "year"), + ("Worked Hours", "worked_hours"), + ("Overtime Hours", "overtime"), + ] + records_per_page = 20 + +@method_decorator(login_required, name="dispatch") +class HourAccountNav(HorillaNavView): + """ + Nav bar + """ + + template_name = "cbv/hour_account/nav_hour_account.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("attendance-ot-search") + if not self.request.user.has_perm( + "attendance.add_attendanceovertime" + ) and not is_reportingmanager(self.request): + self.create_attrs = None + else: + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse_lazy('attendance-overtime-create')}" + """ + actions = [ + { + "action": _("Export"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#hourAccountExport" + hx-get="{reverse('hour-account-export')}" + hx-target="#hourAccountExportModalBody" + style="cursor: pointer;" + """, + } + ] + + if self.request.user.has_perm("attendance.add_attendanceovertime"): + actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + hourAccountbulkDelete(); + " + data-action = "delete" + style="cursor: pointer; color:red !important" + """, + }, + ) + + if not self.request.user.has_perm( + "attendance.add_attendanceovertime" + ) and not is_reportingmanager(self.request): + actions = None + + self.actions = actions + + nav_title = _("Hour Account") + filter_instance = AttendanceOverTimeFilter() + filter_body_template = "cbv/hour_account/hour_filter.html" + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", _("Employee")), + ("month", _("Month")), + ("year", _("Year")), + ("employee_id__country", _("Country")), + ("employee_id__employee_work_info__reporting_manager_id", _("Reporting Manager")), + ("employee_id__employee_work_info__shift_id", _("Shift")), + ("employee_id__employee_work_info__work_type_id", _("Work Type")), + ("employee_id__employee_work_info__department_id", _("Department")), + ("employee_id__employee_work_info__job_position_id", _("Job Position")), + ("employee_id__employee_work_info__employee_type_id", _("Employment Type")), + ("employee_id__employee_work_info__company_id", _("Company")), + ] + +@method_decorator(login_required, name="dispatch") +class HourExportView(TemplateView): + """ + For candidate export + """ + + template_name = "cbv/hour_account/hour_export.html" + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + attendances = AttendanceOverTime.objects.all() + export_fields = AttendanceOverTimeExportForm + export_obj = AttendanceOverTimeFilter(queryset=attendances) + context["export_fields"] = export_fields + context["export_obj"] = export_obj + return context + +@method_decorator(login_required, name="dispatch") +class HourAccountDetailView(HorillaDetailedView): + """ + Detail View + """ + + model = AttendanceOverTime + title = _("Details") + + header = { + "title": "employee_id__get_full_name", + "subtitle": "hour_account_subtitle", + "avatar": "employee_id__get_avatar", + } + + body = [ + (_("Month"), "get_month_capitalized"), + (_("Year"), "year"), + (_("Worked Hours"), "worked_hours"), + (_("Pending Hours"), "pending_hours"), + (_("Over time"), "overtime"), + ] + + action_method = "hour_detail_actions" + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("attendance.add_attendanceovertime"),name='dispatch') +class HourAccountFormView(HorillaFormView): + """ + Form View + """ + + model = AttendanceOverTime + form_class = AttendanceOverTimeForm + # template_name = "cbv/recruitment/forms/create_form.html" + new_display_title = _("Hour Account") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form_class(initial={"employee_id": self.request.user.employee_get}) + if self.form.instance.pk: + self.form_class.verbose_name = _("Hour account update") + self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, self.form, "attendance.add_attendanceovertime" + ) + context["form"] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + # form = self.form_class(self.request.POST) + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: AttendanceOverTimeForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Attendance account updated") + else: + message = _("Attendance account added") + form.save() + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) diff --git a/attendance/cbv/late_come_and_early_out.py b/attendance/cbv/late_come_and_early_out.py new file mode 100644 index 000000000..eff1fb870 --- /dev/null +++ b/attendance/cbv/late_come_and_early_out.py @@ -0,0 +1,236 @@ +""" +Late come and early out page +""" +from typing import Any +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from attendance.filters import LateComeEarlyOutFilter +from attendance.forms import LateComeEarlyOutExportForm +from attendance.models import AttendanceLateComeEarlyOut +from base.filters import PenaltyFilter +from base.methods import filtersubordinates, is_reportingmanager +from base.models import PenaltyAccounts +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaListView, + HorillaNavView, + TemplateView, + HorillaDetailedView, +) + +@method_decorator(login_required, name="dispatch") +class LateComeAndEarlyOut(TemplateView): + """ + Late come and early out + """ + + template_name = "cbv/late_come_and_early_out/late_come_and_early_out.html" + +@method_decorator(login_required, name="dispatch") +class LateComeAndEarlyOutList(HorillaListView): + """ + List view + """ + + filter_keys_to_remove = [ + "late_early_id" + ] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("late-come-early-out-search") + self.view_id = "late-container" + if ( + not self.request.user.has_perm("attendance.chanage_penaltyaccount") + and not is_reportingmanager(self.request) + and not self.request.user.has_perm( + "perms.attendance.delete_attendancelatecomeearlyout" + ) + ): + self.action_method = None + else: + self.action_method = "actions_column" + + def get_queryset(self): + queryset = super().get_queryset() + reports = queryset + self_reports = queryset.filter(employee_id__employee_user_id=self.request.user) + reports = filtersubordinates( + self.request, reports, "attendance.view_attendancelatecomeearlyout" + ) + queryset = self_reports | reports + return queryset + + row_attrs = """ + hx-get='{late_come_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + model = AttendanceLateComeEarlyOut + filter_class = LateComeEarlyOutFilter + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Type"), "get_type"), + (_("Attendance Date"), "attendance_id__attendance_date"), + (_("Check-In"), "attendance_id__attendance_clock_in"), + (_("In Date"), "attendance_id__attendance_clock_in_date"), + (_("Check-Out"), "attendance_id__attendance_clock_out"), + (_("Out Date"), "attendance_id__attendance_clock_out_date"), + (_("Min Hour"), "attendance_id__minimum_hour"), + (_("At Work"), "attendance_id__attendance_worked_hour"), + (_("Penalities"), "penalities_column"), + ] + + header_attrs = { + "penalities_column" :""" + style ="width:170px !important" + """ + } + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Type", "get_type"), + ("Attendance Date", "attendance_id__attendance_date"), + ("Check-In", "attendance_id__attendance_clock_in"), + ("In Date", "attendance_id__attendance_clock_in_date"), + ("Check-Out", "attendance_id__attendance_clock_out"), + ("Out Date", "attendance_id__attendance_clock_out_date"), + ("At Work", "attendance_id__attendance_worked_hour"), + ("Min Hour", "attendance_id__minimum_hour"), + + ] + +@method_decorator(login_required, name="dispatch") +class LateComeAndEarlyOutListNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("late-come-early-out-search") + actions = [ + { + "action": _("Export"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#attendanceExport" + hx-get="{reverse('late-come-and-early-out-export')}" + hx-target="#attendanceExportForm" + style="cursor: pointer;" + """, + } + ] + + if self.request.user.has_perm( + "attendance.chanage_penaltyaccount" + ) or self.request.user.has_perm( + "perms.attendance.delete_attendancelatecomeearlyout" + ): + actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + lateComeBulkDelete(); + " + data-action = "delete" + style="cursor: pointer; color:red !important" + """, + }, + ) + + if ( + not self.request.user.has_perm("attendance.chanage_penaltyaccount") + and not is_reportingmanager(self.request) + and not self.request.user.has_perm( + "perms.attendance.delete_attendancelatecomeearlyout" + ) + ): + actions = None + + self.actions = actions + + nav_title = _("Late Come/Early Out ") + filter_instance = LateComeEarlyOutFilter() + filter_body_template = "cbv/late_come_and_early_out/late_early_filter.html" + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", _("Employee")), + ("type", _("Type")), + ("attendance_id__attendance_date", _("Attendance Date")), + ("attendance_id__shift_id", _("Shift")), + ("attendance_id__work_type_id", _("Work Type")), + ("attendance_id__minimum_hour", _("Minimum Hour")), + ("attendance_id__employee_id__country", _("Country")), + ( + "attendance_id__employee_id__employee_work_info__reporting_manager_id", + _("Reporting Manager"), + ), + ("attendance_id__employee_id__employee_work_info__department_id", _("Department")), + ( + "attendance_id__employee_id__employee_work_info__job_position_id", + _("Job Position"), + ), + ( + "attendance_id__employee_id__employee_work_info__employee_type_id", + _("Employment Type"), + ), + ("attendance_id__employee_id__employee_work_info__company_id", _("Company")), + ] + + +@method_decorator(login_required, name="dispatch") +class LateEarlyExportView(TemplateView): + """ + For export records + """ + + template_name = "cbv/late_come_and_early_out/late_early_export.html" + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + data = AttendanceLateComeEarlyOut.objects.all() + export_form = LateComeEarlyOutExportForm + export = LateComeEarlyOutFilter(queryset=data) + context["export_form"] = export_form + context["export"] = export + return context + +@method_decorator(login_required, name="dispatch") +class LateComeEarlyOutDetailView(HorillaDetailedView): + """ + Detail View + """ + + model = AttendanceLateComeEarlyOut + title = _("Details") + + header = { + "title": "employee_id__get_full_name", + "subtitle": "late_come_subtitle", + "avatar": "employee_id__get_avatar", + } + + body = [ + (_("Type"), "get_type"), + (_("Attendance Date"), "attendance_id__attendance_date"), + (_("Check-In"), "attendance_id__attendance_clock_in"), + (_("Chen-in Date"), "attendance_id__attendance_clock_in_date"), + (_("Check-Out"), "attendance_id__attendance_clock_out"), + (_("Check-out Date"), "attendance_id__attendance_clock_out_date"), + (_("Min Hour"), "attendance_id__minimum_hour"), + (_("At Work"), "attendance_id__attendance_worked_hour"), + (_("Shift"), "attendance_id__shift_id"), + (_("Work Type"), "attendance_id__work_type_id"), + (_("Attendance Validated"), "attendance_validated_check"), + (_("Penalities"), "penalities_column"), + ] + + action_method = "detail_actions" + diff --git a/attendance/cbv/my_attendances.py b/attendance/cbv/my_attendances.py new file mode 100644 index 000000000..f699fd768 --- /dev/null +++ b/attendance/cbv/my_attendances.py @@ -0,0 +1,202 @@ +""" +My attendances +""" + +from typing import Any + +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from attendance.filters import AttendanceFilters +from attendance.models import Attendance +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaListView, + HorillaNavView, + TemplateView, + HorillaDetailedView, +) + + +@method_decorator(login_required, name="dispatch") +class MyAttendances(TemplateView): + """ + My attendances + """ + + template_name = "cbv/my_attendances/my_attendances.html" + +class MyAttendancesListView(HorillaListView): + + + + model = Attendance + filter_class = AttendanceFilters + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("In Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Pending Hour"), "hours_pending"), + (_("Overtime"), "attendance_overtime"), + ] + + row_attrs = """ + hx-get='{my_attendance_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + records_per_page = 20 + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Date", "attendance_date"), + ("Day", "attendance_day__day"), + ("Check-In", "attendance_clock_in"), + ("Shift", "shift_id__employee_shift"), + ("Work Type", "work_type_id__work_type"), + ("Min Hour", "minimum_hour"), + ("Pending Hour", "hours_pending"), + ("In Date", "attendance_clock_in_date"), + ("Check-Out", "attendance_clock_out"), + ("Out Date", "attendance_clock_out_date"), + ("At Work", "attendance_worked_hour"), + ("Overtime", "attendance_overtime"), + ] + + + +@method_decorator(login_required, name="dispatch") +class MyAttendanceList(MyAttendancesListView): + """ + List view + """ + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("my-attendance-list") + + row_status_indications = [ + ( + "approved-request--dot", + _("Approved Request"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=is_validate_request_approved]').val('true'); + $('[name=attendance_validated]').val('unknown').change(); + $('[name=is_validate_request]').val('unknown').change(); + $('#applyFilter').click(); + + " + """, + ), + ( + "requested--dot", + _("Requested"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=is_validate_request]').val('true'); + $('[name=attendance_validated]').val('unknown').change(); + $('[name=is_validate_request_approved]').val('unknown').change(); + $('#applyFilter').click(); + + " + """, + ), + ( + "not-validated--dot", + _("Not Validated"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=attendance_validated]').val('false'); + $('[name=is_validate_request]').val('unknown').change(); + $('[name=is_validate_request_approved]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "validated--dot", + _("Validated"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=attendance_validated]').val('true'); + $('[name=is_validate_request]').val('unknown').change(); + $('[name=is_validate_request_approved]').val('unknown').change(); + $('#applyFilter').click(); + + " + """, + ), + ] + + row_status_class = "validated-{attendance_validated} requested-{is_validate_request} approved-request-{is_validate_request_approved}" + + def get_queryset(self): + queryset = super().get_queryset() + employee = self.request.user.employee_get + queryset = queryset.filter(employee_id=employee) + return queryset + + + + + +@method_decorator(login_required, name="dispatch") +class MyAttendancestNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("my-attendance-list") + self.search_in = [ + ("shift_id__employee_shift", "Shift"), + ("work_type_id__work_type", "Work Type"), + ] + + nav_title = _("My Attendances") + filter_body_template = "cbv/my_attendances/my_attendance_filter.html" + filter_instance = AttendanceFilters() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +class MyAttendancesDetailView(HorillaDetailedView): + """ + Detail View + """ + + model = Attendance + + title = _("Details") + + header = { + "title": "employee_id__get_full_name", + "subtitle": "my_attendance_subtitle", + "avatar": "employee_id__get_avatar", + } + + body = [ + (_("Date"), "attendance_date"), + (_("Day"), "attendance_day"), + (_("Check-In"), "attendance_clock_in"), + (_("Check-in Date"), "attendance_clock_in_date"), + (_("Check-Out"), "attendance_clock_out"), + (_("Check-out Date"), "attendance_clock_out_date"), + (_("Shift"), "shift_id"), + (_("Work Type"), "work_type_id"), + (_("Min Hour"), "minimum_hour"), + (_("At Work"), "attendance_worked_hour"), + (_("Pending Hour"), "hours_pending"), + (_("Overtime"), "attendance_overtime"), + ] diff --git a/attendance/filters.py b/attendance/filters.py index f8d928f34..416e9ffed 100644 --- a/attendance/filters.py +++ b/attendance/filters.py @@ -16,14 +16,17 @@ from django.utils.translation import gettext_lazy as _ from attendance.models import ( Attendance, AttendanceActivity, + AttendanceGeneralSetting, AttendanceLateComeEarlyOut, AttendanceOverTime, + AttendanceValidationCondition, + GraceTime, strtime_seconds, ) from base.filters import FilterSet from employee.filters import EmployeeFilter from employee.models import Employee -from horilla.filters import filter_by_name +from horilla.filters import HorillaFilterSet, filter_by_name class DurationInSecondsFilter(django_filters.CharFilter): @@ -50,7 +53,7 @@ class DurationInSecondsFilter(django_filters.CharFilter): return qs -class AttendanceOverTimeFilter(FilterSet): +class AttendanceOverTimeFilter(HorillaFilterSet): """ Filter set class for AttendanceOverTime model @@ -126,7 +129,7 @@ class AttendanceOverTimeFilter(FilterSet): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" -class LateComeEarlyOutFilter(FilterSet): +class LateComeEarlyOutFilter(HorillaFilterSet): """ LateComeEarlyOutFilter class """ @@ -246,7 +249,7 @@ class LateComeEarlyOutFilter(FilterSet): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" -class AttendanceActivityFilter(FilterSet): +class AttendanceActivityFilter(HorillaFilterSet): """ Filter set class for AttendanceActivity model @@ -329,7 +332,7 @@ class AttendanceActivityFilter(FilterSet): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" -class AttendanceFilters(FilterSet): +class AttendanceFilters(HorillaFilterSet): """ Filter set class for Attendance model @@ -339,6 +342,8 @@ class AttendanceFilters(FilterSet): id = django_filters.NumberFilter(field_name="id") search = django_filters.CharFilter(method="filter_by_name") + search_field = django_filters.CharFilter(method="search_in") + employee = django_filters.CharFilter(field_name="employee_id__id") date_attendance = django_filters.DateFilter(field_name="attendance_date") employee_id = django_filters.ModelMultipleChoiceFilter( @@ -489,6 +494,9 @@ class AttendanceFilters(FilterSet): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" def filter_by_name(self, queryset, name, value): + + if self.data.get("search_field"): + return queryset # Call the imported function """ This method allows filtering by the employee's first and/or last name or by other @@ -651,6 +659,54 @@ class AttendanceRequestReGroup: ] +class AttendanceBreakpointFilter(FilterSet): + """ + filter class for attendance breakpoint condition model + """ + + search = django_filters.CharFilter(field_name="company_id", lookup_expr="icontains") + + class Meta: + model = AttendanceValidationCondition + fields = [ + "validation_at_work", + "minimum_overtime_to_approve", + "overtime_cutoff", + "company_id", + ] + + +class GraceTimeFilter(HorillaFilterSet): + + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = GraceTime + fields = ["company_id"] + + def search_method(self, queryset, _, value): + """ + This method is used to mail server + """ + + return ((queryset.filter(company_id__company__icontains=value))).distinct() + + +class AttendanceGeneralSettingFilter(HorillaFilterSet): + + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = AttendanceGeneralSetting + fields = ["company_id"] + + def search_method(self, queryset, _, value): + """ + This method is used to mail server + """ + return ((queryset.filter(company_id__company__icontains=value))).distinct() + + def get_working_today(queryset, _name, value): today = datetime.datetime.now().date() yesterday = today - datetime.timedelta(days=1) diff --git a/attendance/forms.py b/attendance/forms.py index 3b5c69362..edf124a71 100644 --- a/attendance/forms.py +++ b/attendance/forms.py @@ -37,6 +37,7 @@ from django.db.models.query import QuerySet from django.forms import DateTimeInput from django.template.loader import render_to_string from django.utils.html import format_html +from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ from attendance.filters import AttendanceFilters @@ -55,6 +56,7 @@ from attendance.models import ( validate_time_format, ) from base.forms import ModelForm as BaseModelForm +from base.forms import MultipleFileField from base.methods import ( filtersubordinatesemployeemodel, get_working_days, @@ -151,7 +153,8 @@ class AttendanceUpdateForm(BaseModelForm): { "id": str(uuid.uuid4()), "hx-include": "#attendanceUpdateForm", - "hx-target": "#attendanceUpdateForm", + "hx-target": "#attendanceUpdateFormFields,#personal", + "hx-trigger": "change", "hx-get": "/attendance/update-fields-based-shift", } ) @@ -302,7 +305,8 @@ class AttendanceForm(BaseModelForm): { "id": str(uuid.uuid4()), "hx-include": "#attendanceCreateForm", - "hx-target": "#attendanceCreateForm", + "hx-target": "#attendanceFormFields,#personal", + "hx-trigger": "change", "hx-get": "/attendance/update-fields-based-shift", } ) @@ -509,6 +513,13 @@ class AttendanceValidationConditionForm(forms.ModelForm): Model form for AttendanceValidationCondition """ + cols = { + "validation_at_work": 12, + "minimum_overtime_to_approve": 12, + "overtime_cutoff": 12, + "company_id": 12, + } + validation_at_work = forms.CharField( required=True, initial="00:00", @@ -560,6 +571,8 @@ class AttendanceRequestForm(BaseModelForm): AttendanceRequestForm """ + cols = {"request_description": 12} + def update_worked_hour_hx_fields(self, field_name): """Update the widget attributes for worked hour fields.""" self.fields[field_name].widget.attrs.update( @@ -576,7 +589,7 @@ class AttendanceRequestForm(BaseModelForm): def __init__(self, *args, **kwargs): if instance := kwargs.get("instance"): - # django forms not showing value inside the date, time html element. + # django forms not showing vaupdate-fields-based-shiftlue inside the date, time html element. # so here overriding default forms instance method to set initial value initial = { "attendance_date": instance.attendance_date.strftime("%Y-%m-%d"), @@ -596,15 +609,16 @@ class AttendanceRequestForm(BaseModelForm): super().__init__(*args, **kwargs) self.fields["attendance_clock_out_date"].required = False self.fields["attendance_clock_out"].required = False - self.fields["shift_id"].widget.attrs.update( - { - "id": str(uuid.uuid4()), - "hx-include": "#attendanceRequestForm", - "hx-target": "#attendanceRequestDiv", - "hx-swap": "outerHTML", - "hx-get": "/attendance/update-fields-based-shift", - } - ) + if not self.instance.pk: + self.fields["shift_id"].widget.attrs.update( + { + "id": str(uuid.uuid4()), + "hx-include": "#attendanceRequestForm", + "hx-target": "#attendanceRequest", + "hx-swap": "innerHTML", + "hx-get": "/attendance/update-fields-based-shift", + } + ) for field in [ "attendance_clock_in_date", "attendance_clock_in", @@ -679,7 +693,8 @@ class NewRequestForm(AttendanceRequestForm): widget=forms.Select( attrs={ "class": "oh-select oh-select-2 w-100", - "hx-target": "#id_shift_id_div", + "hx-target": "#id_shift_id_parent_div,#id_shift_id_div", + "hx-swap": "innerHTML", "hx-get": "/attendance/get-employee-shift?bulk=False", } ), @@ -691,15 +706,15 @@ class NewRequestForm(AttendanceRequestForm): widget=forms.CheckboxInput( attrs={ "class": "oh-checkbox", - "hx-target": "#objectCreateModalTarget", - "hx-get": "/attendance/request-new-attendance?bulk=True", + "hx-target": "#genericModalBody", + "hx-swap": "innerHTML", + "hx-get": "/attendance/request-bulk-attendance?bulk=True", } ), ), } new_dict.update(old_dict) self.fields = new_dict - kwargs["initial"] = view_initial def as_p(self, *args, **kwargs): @@ -918,6 +933,8 @@ class GraceTimeForm(BaseModelForm): Form for create or update Grace time """ + cols = {"allowed_time": 12, "company_id": 12, "shifts": 12} + shifts = forms.ModelMultipleChoiceField( queryset=EmployeeShift.objects.all(), required=False, @@ -1036,7 +1053,8 @@ class BulkAttendanceRequestForm(BaseModelForm): queryset=Employee.objects.filter(is_active=True), widget=forms.Select( attrs={ - "hx-target": "#id_shift_id_div", + "hx-target": "#id_shift_id_parent_div", + "hx-swap": "innerHTML", "hx-get": "/attendance/get-employee-shift?bulk=True", } ), @@ -1049,7 +1067,8 @@ class BulkAttendanceRequestForm(BaseModelForm): widget=forms.CheckboxInput( attrs={ "class": "oh-checkbox", - "hx-target": "#objectCreateModalTarget", + "hx-target": "#genericModalBody", + "hx-swap": "innerHTML", "hx-get": "/attendance/request-new-attendance?bulk=False", } ), diff --git a/attendance/methods/utils.py b/attendance/methods/utils.py index 5c19e2ee2..814d35776 100644 --- a/attendance/methods/utils.py +++ b/attendance/methods/utils.py @@ -551,6 +551,14 @@ def parse_time(time_str): return None +def parse_datetime(date_str, time_str): + return ( + datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M") + if date_str and time_str + else None + ) + + def parse_date(date_str, error_key, activity): try: return pd.to_datetime(date_str).date() diff --git a/attendance/models.py b/attendance/models.py index f16ff0d64..7946cfcca 100644 --- a/attendance/models.py +++ b/attendance/models.py @@ -10,10 +10,12 @@ import datetime as dt import json from datetime import date, datetime, timedelta +import pandas as pd from django.apps import apps from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q +from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -31,9 +33,12 @@ from base.horilla_company_manager import HorillaCompanyManager from base.methods import is_company_leave, is_holiday from base.models import Company, EmployeeShift, EmployeeShiftDay, WorkType from employee.models import Employee + +# Create your models here. from horilla.methods import get_horilla_model_class from horilla.models import HorillaModel from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog +from horilla_views.cbv_methods import render_template # to skip the migration issue with the old migrations _validate_time_in_minutes = validate_time_in_minutes @@ -81,6 +86,70 @@ class AttendanceActivity(HorillaModel): ordering = ["-attendance_date", "employee_id__employee_first_name", "clock_in"] + def get_status(self): + """ + Display status + """ + + DAY = [ + ("monday", _("Monday")), + ("tuesday", _("Tuesday")), + ("wednesday", _("Wednesday")), + ("thursday", _("Thursday")), + ("friday", _("Friday")), + ("saturday", _("Saturday")), + ("sunday", _("Sunday")), + ] + return dict(DAY).get(self.shift_day.day) + + def get_delete_attendance(self): + """ + for delete button + """ + + return render_template( + path="cbv/attendance_activity/delete_action.html", + context={"instance": self}, + ) + + def attendance_detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.employee_id.employee_work_info.department_id} / {self.employee_id.employee_work_info.job_position_id}" + + def attendance_detail_view(self): + """ + for detail view of page + """ + url = reverse("attendance-activity-single-view", kwargs={"pk": self.pk}) + return url + + def diff_cell(self): + if self.clock_out == None: + return 'style="background-color: #FFE4B3"' + + def detail_view_delete_attendance(self): + """ + for delete button + """ + + return render_template( + path="cbv/attendance_activity/detail_delete_action.html", + context={"instance": self}, + ) + + def duration_format_time(self, seconds): + """ + This method is used to format seconds to H:M:S and return it + args: + seconds : seconds + """ + hour = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + seconds = int((seconds % 3600) % 60) + return f"{hour:02d}:{minutes:02d}:{seconds:02d}" + def duration(self): """ Duration calc b/w in-out method @@ -97,6 +166,15 @@ class AttendanceActivity(HorillaModel): return time_difference.total_seconds() + def duration_format(self): + """ + Function to return the duration time in hh:mm:ss + """ + total_seconds = self.duration() + formatted_duration = self.duration_format_time(total_seconds) + + return formatted_duration + def __str__(self): return f"{self.employee_id} - {self.attendance_date} - {self.clock_in} - {self.clock_out}" @@ -123,7 +201,7 @@ class Attendance(HorillaModel): status = [ ("create_request", _("Create Request")), ("update_request", _("Update Request")), - ("revalidate_request", _("Re-validate Request")), + ("created_request", _("Created Request")), ] employee_id = models.ForeignKey( @@ -228,6 +306,49 @@ class Attendance(HorillaModel): ], ) + def get_instance_id(self): + return self.id + + def diff_cell(self): + if self.request_type == "created_request": + return 'style="background-color: #FFE4B3"' + + def status_col(self): + """ + This method for get custome coloumn for rating. + """ + + return render_template( + path="cbv/attendance_request/status.html", + context={"instance": self}, + ) + + def my_attendance_subtitle(self): + """ + Detail view subtitle + """ + + return f"""{self.employee_id.employee_work_info.department_id } / + { self.employee_id.employee_work_info.job_position_id}""" + + def my_attendance_detail(self): + """ + detail view + """ + + url = reverse("my-attendance-detail", kwargs={"pk": self.pk}) + + return url + + def attendance_detail_view(self): + """ + detail view + """ + + url = reverse("attendances-tab-detail-view", kwargs={"pk": self.pk}) + + return url + class Meta: """ Meta class to add some additional options @@ -276,6 +397,162 @@ class Attendance(HorillaModel): ) return {"query": activities, "count": activities.count()} + def attendance_actions(self): + """ + method for rendering actions(edit,delete) + """ + + return render_template( + path="cbv/attendances/attendance_actions.html", + context={"instance": self}, + ) + + def comment_col(self): + """ + This method for get custom coloumn for comment. + """ + + return render_template( + path="cbv/attendance_request/comment.html", + context={"instance": self}, + ) + + def attendance_detail_activity_col(self): + """ + this method is used to return attendance detail view activity custom col + """ + + return render_template( + path="cbv/attendances/detail_view_activity_col.html", + context={"instance": self}, + ) + + def request_actions(self): + """ + This method for get custom coloumn for comment. + """ + + return render_template( + path="cbv/attendance_request/request_actions.html", + context={"instance": self}, + ) + + def validate_detail_view(self): + """ + detail view of validate tab + """ + url = reverse("validate-detail-view", kwargs={"pk": self.pk}) + return url + + def individual_validate_detail_view(self): + """ + detail view of validate tab + """ + url = reverse("individual-validate-detail-view", kwargs={"pk": self.pk}) + return url + + def ot_detail_view(self): + """ + detail view of OT tab + """ + url = reverse("ot-detail-view", kwargs={"pk": self.pk}) + return url + + def validated_detail_view(self): + """ + detail view of validated tab + """ + url = reverse("validated-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_view(self): + """ + deteil view of requested attendances + """ + url = reverse("validate-attendance-request", kwargs={"attendance_id": self.pk}) + return url + + def change_attendance(self): + """ + Edit url + """ + url = reverse("update-attendance-request", kwargs={"pk": self.pk}) + return url + + def ot_approve(self): + """ + method for rendering approve OT + """ + minot = strtime_seconds("00:30") + condition = AttendanceValidationCondition.objects.first() + if condition is not None: + minot = strtime_seconds(condition.minimum_overtime_to_approve) + + return render_template( + path="cbv/attendances/ot_confirmation.html", + context={"instance": self, "minot": minot}, + ) + + def validate_detail_actions(self): + """ + detail view actions of validate tab + """ + + return render_template( + path="cbv/attendances/validate_tab_action.html", + context={"instance": self}, + ) + + def ot_detail_actions(self): + """ + detail view actions of OT tab + """ + + minot = strtime_seconds("00:30") + condition = AttendanceValidationCondition.objects.first() + if condition is not None: + minot = strtime_seconds(condition.minimum_overtime_to_approve) + + return render_template( + path="cbv/attendances/ot_tab_action.html", + context={"instance": self, "minot": minot}, + ) + + def validated_detail_actions(self): + """ + detail view actions of validated tab + """ + + return render_template( + path="cbv/attendances/validated_tab_action.html", + context={"instance": self}, + ) + + def validate_button(self): + """ + detail view actions of validated tab + """ + + return render_template( + path="cbv/attendances/validate_button.html", + context={"instance": self}, + ) + + def attendances_detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.employee_id.employee_work_info.department_id} / {self.employee_id.employee_work_info.job_position_id}" + + def activities(self): + """ + This method is used to return the activites and count of activites comes for an attendance + """ + activities = AttendanceActivity.objects.filter( + attendance_date=self.attendance_date, employee_id=self.employee_id + ) + return {"query": activities, "count": activities.count()} + def requested_fields(self): """ This method will returns the value difference fields @@ -419,6 +696,7 @@ class Attendance(HorillaModel): attendance_account.overtime = format_time(total_ot_seconds) attendance_account.save() super().save(*args, **kwargs) + self.first_save = False def serialize(self): """ @@ -663,6 +941,69 @@ class AttendanceOverTime(HorillaModel): verbose_name = _("Hour Account") verbose_name_plural = _("Hour Accounts") + def get_month_capitalized(self): + """ + capitalize month + """ + return self.month.capitalize() + + def edit_url_overtime(self): + """ + Edit url + """ + + url = reverse("attendance-overtime-update", kwargs={"obj_id": self.pk}) + + return url + + def delete_url_overtime(self): + """ + delete url + """ + + url = reverse("attendance-overtime-delete", kwargs={"obj_id": self.pk}) + + return url + + def hour_actions(self): + """ + actions in hour account + + """ + + return render_template( + path="cbv/hour_account/hour_actions.html", + context={"instance": self}, + ) + + def hour_account_subtitle(self): + """ + Detail view subtitle + """ + + return f"""{self.employee_id.employee_work_info.department_id } / + { self.employee_id.employee_work_info.job_position_id}""" + + def hour_account_detail(self): + """ + detail view + """ + + url = reverse("hour-account-detail-view", kwargs={"pk": self.pk}) + + return url + + def hour_detail_actions(self): + """ + actions in hour account detail view + + """ + + return render_template( + path="cbv/hour_account/hour_detail_action.html", + context={"instance": self}, + ) + def clean(self): try: year = int(self.year) @@ -800,6 +1141,72 @@ class AttendanceLateComeEarlyOut(HorillaModel): unique_together = [("attendance_id"), ("type")] ordering = ["-attendance_id__attendance_date"] + def get_type(self): + """ + Display work type + """ + choices = [ + ("late_come", _("Late Come")), + ("early_out", _("Early Out")), + ] + return dict(choices).get(self.type) + + def penalities_column(self): + """ + To get penalities + + """ + + return render_template( + path="cbv/late_come_and_early_out/penality.html", + context={"instance": self}, + ) + + def actions_column(self): + """ + actions in hour account + + """ + + return render_template( + path="cbv/late_come_and_early_out/actions_column.html", + context={"instance": self}, + ) + + def detail_actions(self): + """ + actions in hour account + + """ + + return render_template( + path="cbv/late_come_and_early_out/detail_action.html", + context={"instance": self}, + ) + + def late_come_subtitle(self): + """ + Detail view subtitle + """ + + return f"""{self.employee_id.employee_work_info.department_id } / + { self.employee_id.employee_work_info.job_position_id}""" + + def attendance_validated_check(self): + if self.attendance_id.attendance_validated == True: + return "Yes" + else: + return "No" + + def late_come_detail(self): + """ + detail view + """ + + url = reverse("late-in-early-out-single-view", kwargs={"pk": self.pk}) + + return url + def __str__(self) -> str: return f"{self.attendance_id.employee_id.employee_first_name} \ {self.attendance_id.employee_id.employee_last_name} - {self.type}" @@ -835,6 +1242,17 @@ class AttendanceValidationCondition(HorillaModel): if not self.id and AttendanceValidationCondition.objects.exists(): raise ValidationError(_("You cannot add more conditions.")) + def break_point_actions(self): + """ + actions in hour account + + """ + + return render_template( + path="cbv/settings/break_point_action.html", + context={"instance": self}, + ) + class GraceTime(HorillaModel): """ @@ -866,6 +1284,65 @@ class GraceTime(HorillaModel): def __str__(self) -> str: return str(f"{self.allowed_time} - Hours") + def get_instance_id(self): + return self.id + + def is_active_col(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/settings/is_active_col_grace_time.html", + context={"instance": self}, + ) + + def allowed_time_col(self): + """ + Allowed time col + """ + return f"{self.allowed_time} Hours" + + def action_col(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/settings/grace_time_default_action.html", + context={"instance": self}, + ) + + def applicable_on_clock_in_col(self): + """ + This method for get custom column . + """ + + return render_template( + path="cbv/settings/applicable_on_clock_in_col.html", + context={"instance": self}, + ) + + def applicable_on_clock_out_col(self): + """ + This method for get custom column . + """ + + return render_template( + path="cbv/settings/applicable_on_clock_out_col.html", + context={"instance": self}, + ) + + def get_shifts_display(self): + """ + This method for get custom column . + """ + + return render_template( + path="cbv/settings/grace_time_shift.html", + context={"instance": self}, + ) + def clean(self): """ This method is used to perform some custom validations @@ -928,6 +1405,22 @@ class AttendanceGeneralSetting(HorillaModel): company_id = models.ForeignKey(Company, on_delete=models.CASCADE, null=True) objects = HorillaCompanyManager() + def company_col(self): + if self.company_id: + return self.company_id.company + else: + return "All Company" + + def check_in_check_out_col(self): + """ + This method for get custom coloumn . + """ + + return render_template( + path="cbv/settings/check_in_check_out_col.html", + context={"instance": self}, + ) + class WorkRecords(models.Model): """ diff --git a/attendance/static/cbv/attendance/hour_account.js b/attendance/static/cbv/attendance/hour_account.js new file mode 100644 index 000000000..e683b9796 --- /dev/null +++ b/attendance/static/cbv/attendance/hour_account.js @@ -0,0 +1,327 @@ + + +var downloadMessages = { + ar: "هل ترغب في تنزيل القالب؟", + de: "Möchten Sie die Vorlage herunterladen?", + es: "¿Quieres descargar la plantilla?", + en: "Do you want to download the template?", + fr: "Voulez-vous télécharger le modèle ?", + }; + var validateMessages = { + ar: "هل ترغب حقًا في التحقق من كل الحضور المحدد؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten überprüfen?", + es: "¿Realmente quieres validar todas las asistencias seleccionadas?", + en: "Do you really want to validate all the selected attendances?", + fr: "Voulez-vous vraiment valider toutes les présences sélectionnées?", + }; + var overtimeMessages = { + ar: "هل ترغب حقًا في الموافقة على الساعات الإضافية لجميع الحضور المحدد؟", + de: "Möchten Sie wirklich die Überstunden für alle ausgewählten Anwesenheiten genehmigen?", + es: "¿Realmente quieres aprobar las horas extras para todas las asistencias seleccionadas?", + en: "Do you really want to approve OT for all the selected attendances?", + fr: "Voulez-vous vraiment approuver les heures supplémentaires pour toutes les présences sélectionnées?", + }; + var hourdeleteMessages = { + ar: "هل ترغب حقًا في حذف جميع الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten löschen?", + es: "¿Realmente quieres eliminar todas las asistencias seleccionadas?", + en: "Do you really want to delete all the selected attendances?", + fr: "Voulez-vous vraiment supprimer toutes les présences sélectionnées?", + }; + var lateDeleteMessages = { + ar: "هل ترغب حقًا في حذف جميع الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten löschen?", + es: "¿Realmente quieres eliminar todas las asistencias seleccionadas?", + en: "Do you really want to delete all the selected records?", + fr: "Voulez-vous vraiment supprimer toutes les présences sélectionnées?", + }; + var noRowValidateMessages = { + ar: "لم يتم تحديد أي صفوف من فحص الحضور.", + de: "Im Feld „Anwesenheit validieren“ sind keine Zeilen ausgewählt.", + es: "No se selecciona ninguna fila de Validar asistencia.", + en: "No rows are selected from Validate Attendances.", + fr: "Aucune ligne n'est sélectionnée dans Valider la présence.", + }; + var norowotMessages = { + ar: "لم يتم تحديد أي صفوف من حضور العمل الإضافي.", + de: "In der OT-Anwesenheit sind keine Zeilen ausgewählt.", + es: "No se seleccionan filas de Asistencias de OT.", + en: "No rows are selected from OT Attendances.", + fr: "Aucune ligne n'est sélectionnée dans les présences OT.", + }; + var norowdeleteMessages = { + ar: "لم يتم تحديد أي صفوف لحذف الحضور.", + de: "Es sind keine Zeilen zum Löschen von Anwesenheiten ausgewählt.", + es: "No se seleccionan filas para eliminar asistencias.", + en: "No rows are selected for deleting attendances.", + fr: "Aucune ligne n'est sélectionnée pour la suppression des présences.", + }; + var lateNorowdeleteMessages = { + ar: "لم يتم تحديد أي صفوف لحذف الحضور.", + de: "Es sind keine Zeilen zum Löschen von Anwesenheiten ausgewählt.", + es: "No se seleccionan filas para eliminar asistencias.", + en: "No rows are selected for deleting records.", + fr: "Aucune ligne n'est sélectionnée pour la suppression des présences.", + }; + var rowMessages = { + ar: " تم الاختيار", + de: " Ausgewählt", + es: " Seleccionado", + en: " Selected", + fr: " Sélectionné", + }; + var excelMessages = { + ar: "هل ترغب في تنزيل ملف Excel؟", + de: "Möchten Sie die Excel-Datei herunterladen?", + es: "¿Desea descargar el archivo de Excel?", + en: "Do you want to download the excel file?", + fr: "Voulez-vous télécharger le fichier Excel?", + }; + var requestAttendanceApproveMessages = { + ar: "هل ترغب حقًا في الموافقة على جميع طلبات الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheitsanfragen genehmigen?", + es: "¿Realmente quieres aprobar todas las solicitudes de asistencia seleccionadas?", + en: "Do you really want to approve all the selected attendance requests?", + fr: "Voulez-vous vraiment approuver toutes les demandes de présence sélectionnées?", + }; + + var reqAttendancRejectMessages = { + ar: "هل ترغب حقًا في رفض جميع طلبات الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheitsanfragen ablehnen?", + es: "¿Realmente quieres rechazar todas las solicitudes de asistencia seleccionadas?", + en: "Do you really want to reject all the selected attendance requests?", + fr: "Voulez-vous vraiment rejeter toutes les demandes de présence sélectionnées?", + }; + + tickCheckboxes(); + function makeListUnique(list) { + return Array.from(new Set(list)); + } + + tickactivityCheckboxes(); + function makeactivityListUnique(list) { + return Array.from(new Set(list)); + } + + ticklatecomeCheckboxes(); + function makelatecomeListUnique(list) { + return Array.from(new Set(list)); + } + + function getCurrentLanguageCode(callback) { + var languageCode = $("#main-section-data").attr("data-lang"); + var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"]; + if (allowedLanguageCodes.includes(languageCode)) { + callback(languageCode); + } else { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var ajaxLanguageCode = response.language_code; + $("#main-section-data").attr("data-lang", ajaxLanguageCode); + callback( + allowedLanguageCodes.includes(ajaxLanguageCode) + ? ajaxLanguageCode + : "en" + ); + }, + error: function () { + callback("en"); + }, + }); + } + } + + + function hourAccountbulkDelete() { + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = hourdeleteMessages[languageCode]; + var textMessage = norowdeleteMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/attendance/attendance-account-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + }; + + + function lateComeBulkDelete() { + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = lateDeleteMessages[languageCode]; + var textMessage = lateNorowdeleteMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/attendance/late-come-early-out-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + }; + + + function reqAttendanceBulkApprove() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = requestAttendanceApproveMessages[languageCode]; + var textMessage = lateNorowdeleteMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = JSON.parse($("#selectedInstances").attr("data-ids") || "[]"); + $.ajax({ + type: "POST", + url: "/attendance/bulk-approve-attendance-request", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + }; + + + function reqAttendanceBulkReject() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = reqAttendancRejectMessages[languageCode]; + var textMessage = noRowValidateMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = JSON.parse($("#selectedInstances").attr("data-ids") || "[]"); + $.ajax({ + type: "POST", + url: "/attendance/bulk-reject-attendance-request", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + }; + + + \ No newline at end of file diff --git a/attendance/static/cbv/attendance_activity.js b/attendance/static/cbv/attendance_activity.js new file mode 100644 index 000000000..6728f1270 --- /dev/null +++ b/attendance/static/cbv/attendance_activity.js @@ -0,0 +1,438 @@ +var downloadMessages = { + ar: "هل ترغب في تنزيل القالب؟", + de: "Möchten Sie die Vorlage herunterladen?", + es: "¿Quieres descargar la plantilla?", + en: "Do you want to download the template?", + fr: "Voulez-vous télécharger le modèle ?", + }; + var validateMessages = { + ar: "هل ترغب حقًا في التحقق من كل الحضور المحدد؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten überprüfen?", + es: "¿Realmente quieres validar todas las asistencias seleccionadas?", + en: "Do you really want to validate all the selected attendances?", + fr: "Voulez-vous vraiment valider toutes les présences sélectionnées?", + }; + var overtimeMessages = { + ar: "هل ترغب حقًا في الموافقة على الساعات الإضافية لجميع الحضور المحدد؟", + de: "Möchten Sie wirklich die Überstunden für alle ausgewählten Anwesenheiten genehmigen?", + es: "¿Realmente quieres aprobar las horas extras para todas las asistencias seleccionadas?", + en: "Do you really want to approve OT for all the selected attendances?", + fr: "Voulez-vous vraiment approuver les heures supplémentaires pour toutes les présences sélectionnées?", + }; + var attendancedeleteMessages = { + ar: "هل ترغب حقًا في حذف جميع الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten löschen?", + es: "¿Realmente quieres eliminar todas las asistencias seleccionadas?", + en: "Do you really want to delete all the selected attendances?", + fr: "Voulez-vous vraiment supprimer toutes les présences sélectionnées?", + }; + var noRowValidateMessages = { + ar: "لم يتم تحديد أي صفوف من فحص الحضور.", + de: "Im Feld „Anwesenheit validieren“ sind keine Zeilen ausgewählt.", + es: "No se selecciona ninguna fila de Validar asistencia.", + en: "No rows are selected from Validate Attendances.", + fr: "Aucune ligne n'est sélectionnée dans Valider la présence.", + }; + var norowotMessages = { + ar: "لم يتم تحديد أي صفوف من حضور العمل الإضافي.", + de: "In der OT-Anwesenheit sind keine Zeilen ausgewählt.", + es: "No se seleccionan filas de Asistencias de OT.", + en: "No rows are selected from OT Attendances.", + fr: "Aucune ligne n'est sélectionnée dans les présences OT.", + }; + var norowdeleteMessages = { + ar: "لم يتم تحديد أي صفوف لحذف الحضور.", + de: "Es sind keine Zeilen zum Löschen von Anwesenheiten ausgewählt.", + es: "No se seleccionan filas para eliminar asistencias.", + en: "No rows are selected for deleting attendances.", + fr: "Aucune ligne n'est sélectionnée pour la suppression des présences.", + }; + var rowMessages = { + ar: " تم الاختيار", + de: " Ausgewählt", + es: " Seleccionado", + en: " Selected", + fr: " Sélectionné", + }; + var excelMessages = { + ar: "هل ترغب في تنزيل ملف Excel؟", + de: "Möchten Sie die Excel-Datei herunterladen?", + es: "¿Desea descargar el archivo de Excel?", + en: "Do you want to download the excel file?", + fr: "Voulez-vous télécharger le fichier Excel?", + }; + var reqAttendanceApproveMessages = { + ar: "هل ترغب حقًا في الموافقة على جميع طلبات الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheitsanfragen genehmigen?", + es: "¿Realmente quieres aprobar todas las solicitudes de asistencia seleccionadas?", + en: "Do you really want to approve all the selected attendance requests?", + fr: "Voulez-vous vraiment approuver toutes les demandes de présence sélectionnées?", + }; + + var reqAttendanceApproveMessages = { + ar: "هل ترغب حقًا في رفض جميع طلبات الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheitsanfragen ablehnen?", + es: "¿Realmente quieres rechazar todas las solicitudes de asistencia seleccionadas?", + en: "Do you really want to reject all the selected attendance requests?", + fr: "Voulez-vous vraiment rejeter toutes les demandes de présence sélectionnées?", + }; + + tickCheckboxes(); + function makeListUnique(list) { + return Array.from(new Set(list)); + } + + tickactivityCheckboxes(); + function makeactivityListUnique(list) { + return Array.from(new Set(list)); + } + + ticklatecomeCheckboxes(); + function makelatecomeListUnique(list) { + return Array.from(new Set(list)); + } + + function getCurrentLanguageCode(callback) { + var languageCode = $("#main-section-data").attr("data-lang"); + var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"]; + if (allowedLanguageCodes.includes(languageCode)) { + callback(languageCode); + } else { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var ajaxLanguageCode = response.language_code; + $("#main-section-data").attr("data-lang", ajaxLanguageCode); + callback( + allowedLanguageCodes.includes(ajaxLanguageCode) + ? ajaxLanguageCode + : "en" + ); + }, + error: function () { + callback("en"); + }, + }); + } + } + + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + + + +function deleteAttendanceNav() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = attendancedeleteMessages[languageCode]; + var textMessage = norowdeleteMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/attendance/attendance-activity-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + } + + + function importAttendanceNav() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = downloadMessages[languageCode]; + Swal.fire({ + text: confirmMessage, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + $.ajax({ + type: "GET", + url: "/attendance/attendance-excel", + dataType: "binary", + xhrFields: { + responseType: "blob", + }, + success: function (response) { + const file = new Blob([response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "attendance_excel.xlsx"; + document.body.appendChild(link); + link.click(); + }, + error: function (xhr, textStatus, errorThrown) { + console.error("Error downloading file:", errorThrown); + }, + }); + } + }); + }); + } + + + + function importAttendanceActivity() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = downloadMessages[languageCode]; + Swal.fire({ + text: confirmMessage, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + $.ajax({ + type: "GET", + url: "/attendance/attendance-activity-import-excel", + dataType: "binary", + xhrFields: { + responseType: "blob", + }, + success: function (response) { + const file = new Blob([response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "activity_excel.xlsx"; + document.body.appendChild(link); + link.click(); + }, + error: function (xhr, textStatus, errorThrown) { + console.error("Error downloading file:", errorThrown); + }, + }); + } + }); + }); + } + + + function bulkDeleteAttendanceNav() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = attendancedeleteMessages[languageCode]; + var textMessage = norowdeleteMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/attendance/attendance-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } + }, + }); + } + }); + } + }); + } + + + + + function showApproveAlert(dataReqValue) { + Swal.fire({ + title: 'Pending Attendance Update Request!', + text: 'An attendance request exists for updating this attendance prior to validation.', + icon: 'warning', + confirmButtonText: 'View Request', + showCancelButton: true, + cancelButtonText: 'Close', + preConfirm: () => { + // Redirect to the page based on dataReqValue + localStorage.setItem("attendanceRequestActiveTab","#tab_1") + window.location.href = dataReqValue; + + }, + }); + } + + + function bulkValidateTabAttendance(dataReqValue) { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = validateMessages[languageCode]; + var textMessage = noRowValidateMessages[languageCode]; + ids = []; + ids.push($("#validateselectedInstances").attr("data-ids")); + ids = JSON.parse($("#validateselectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#validateselectedInstances").attr("data-ids")); + ids = JSON.parse($("#validateselectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/attendance/validate-bulk-attendance", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } else { + } + }, + }); + } + }); + } + }); + } + + function otBulkValidateTabAttendance(dataReqValue) { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = overtimeMessages[languageCode]; + var textMessage = norowotMessages[languageCode]; + ids = []; + ids.push($("#overtimeselectedInstances").attr("data-ids")); + ids = JSON.parse($("#overtimeselectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "success", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#overtimeselectedInstances").attr("data-ids")); + ids = JSON.parse($("#overtimeselectedInstances").attr("data-ids")); + + $.ajax({ + type: "POST", + url: "/attendance/approve-bulk-overtime", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); + } else { + } + }, + }); + } + }); + } + }); + } + + \ No newline at end of file diff --git a/attendance/templates/attendance/attendance_activity/export_filter.html b/attendance/templates/attendance/attendance_activity/export_filter.html index 5c43be942..9ca69bade 100644 --- a/attendance/templates/attendance/attendance_activity/export_filter.html +++ b/attendance/templates/attendance/attendance_activity/export_filter.html @@ -1,17 +1,5 @@ {% load static %} {% load i18n %} -
    -

    - {% trans "Export Attendance Activities" %} -

    - -
    -
    -
    - {% csrf_token %} +
    {% trans "Excel columns" %}
    @@ -144,8 +132,3 @@
    - - -
    diff --git a/attendance/templates/attendance/break_point/condition.html b/attendance/templates/attendance/break_point/condition.html index 910d2bccc..784acc818 100644 --- a/attendance/templates/attendance/break_point/condition.html +++ b/attendance/templates/attendance/break_point/condition.html @@ -1,4 +1,8 @@ -{% extends 'settings.html' %} {% load i18n %} {% block settings %}{% load static %} +{% extends 'settings.html' %} +{% load i18n %} +{% block settings %} +{% load static %} +{% include "generic/components.html" %}
    @@ -14,8 +18,9 @@ {% endif %}
    - {% if condition %} -
    +
    + {% comment %} {% if condition %} {% endcomment %} + {% comment %}
    @@ -35,12 +40,26 @@
    {% trans "Actions" %}
    {% endif %}
    -
    -
    - {% if condition != None %} -
    +
    {% endcomment %} + {% comment %}
    {% endcomment %} + {% comment %} {% if condition != None %} {% endcomment %} + +
    +
    +
    +
    + {% comment %}
    {{ condition.validation_at_work }}
    {{ condition.minimum_overtime_to_approve }}
    +
    {{ condition.overtime_cutoff }}
    {% endcomment %} + {% comment %} {% if perms.attendance.change_attendancevalidationcondition %}
    {{ condition.overtime_cutoff }}
    {{ condition.auto_approve_ot|yesno:"Yes,No" }}
    {% if perms.attendance.change_attendancevalidationcondition %} @@ -49,17 +68,18 @@ hx-target="#objectUpdateModalTarget" type="button" class="oh-btn oh-btn--info" data-toggle="oh-modal-toggle" data-target="#objectUpdateModal">{% trans 'Edit' %}
    - {% endif %} -
    - {% endif %} -
    + {% endif %} {% endcomment %} + {% comment %}
    {% endcomment %} + {% comment %} {% endif %} {% endcomment %} + {% comment %}
    {% endcomment %} + {% comment %}
    - - {% else %} + {% endcomment %} + {% comment %} {% else %}
    Page not found. 404.
    {% trans "There is no attendance conditions at this moment." %}
    -
    - {% endif %} + {% endcomment %} + {% comment %} {% endif %} {% endcomment %} {% endblock %} diff --git a/attendance/templates/attendance/dashboard/dashboard.html b/attendance/templates/attendance/dashboard/dashboard.html index f92d97e34..ba0b06f4c 100644 --- a/attendance/templates/attendance/dashboard/dashboard.html +++ b/attendance/templates/attendance/dashboard/dashboard.html @@ -1,5 +1,6 @@ {% extends 'index.html' %} {% load i18n %} {% block content %} {% load static %} {% load attendancefilters %} +{% include 'generic/components.html' %} + + + + + + +
    @@ -104,9 +123,11 @@ {% trans 'Offline Employees' %}
    -
    -
    +
    +
    +
    +
    diff --git a/attendance/templates/attendance/grace_time/grace_time_table.html b/attendance/templates/attendance/grace_time/grace_time_table.html index afa23d202..f3ee25c0e 100644 --- a/attendance/templates/attendance/grace_time/grace_time_table.html +++ b/attendance/templates/attendance/grace_time/grace_time_table.html @@ -1,4 +1,5 @@ {% load static %}{% load i18n %} +{% include "generic/components.html" %}
    {% if messages %}
    @@ -12,7 +13,11 @@
    {% endif %}
    -
    + +
    +
    + + {% comment %}

    {% trans 'Default Grace Time' %}

    {% if not default_grace_time and perms.attendance.add_gracetime %}
    - {% if default_grace_time %} +
    {% endcomment %} + + + + + + +
    +
    +
    +
    + {% comment %} {% if default_grace_time %}
    @@ -121,13 +146,15 @@ src="{% static 'images/ui/Hour_glass.png' %}" class="" alt="Page not found. 404." />
    {% trans "There is no default grace time at this moment." %}
    - {% endif %} + {% endif %} {% endcomment %}
    -
    +
    +
    + {% comment %}

    {% trans 'Grace Time' %}

    {% if perms.attendance.add_gracetime %}
    {% endcomment %} + + + + + +
    +
    +
    - {% if grace_times %} + {% comment %} {% if grace_times %}
    @@ -257,7 +303,7 @@ src="{% static 'images/ui/Hour_glass.png' %}" class="" alt="Page not found. 404." />
    {% trans "There is no grace time at this moment." %}
    - {% endif %} + {% endif %} {% endcomment %}
    diff --git a/attendance/templates/attendance/penalty/form.html b/attendance/templates/attendance/penalty/form.html index 00a077ae1..c52d01803 100644 --- a/attendance/templates/attendance/penalty/form.html +++ b/attendance/templates/attendance/penalty/form.html @@ -14,6 +14,9 @@ spanElement.click(); } }, 1000); + $(document).ready(function(){ + $('.reload-record').click(); + }) {% if late_in_early_out_ids %} @@ -82,8 +85,7 @@ {% trans "By default minus leave will cut/deduct from available leaves" %}
  • - {% trans "By enabling 'Deduct from carry forward' leave will cut/deduct from carry forward days" - %} + {% trans "By enabling 'Deduct from carry forward' leave will cut/deduct from carry forward days" %}
  • {% endif %} diff --git a/attendance/templates/attendance/settings/check_in_check_out_enable_form.html b/attendance/templates/attendance/settings/check_in_check_out_enable_form.html index b8ea2fbcb..b85195cba 100644 --- a/attendance/templates/attendance/settings/check_in_check_out_enable_form.html +++ b/attendance/templates/attendance/settings/check_in_check_out_enable_form.html @@ -2,10 +2,21 @@
    -
    + {% comment %}

    {% trans 'Enable Check In/Check out' %}

    +
    {% endcomment %} +
    +
    + +
    +
    +
    -
    + {% comment %}
    @@ -53,6 +64,6 @@ {% endfor %}
    -
    +
    {% endcomment %}
    {% endblock %} diff --git a/attendance/templates/attendance/work_record/work_record_view.html b/attendance/templates/attendance/work_record/work_record_view.html index da0e8da0a..5a64a87eb 100644 --- a/attendance/templates/attendance/work_record/work_record_view.html +++ b/attendance/templates/attendance/work_record/work_record_view.html @@ -62,8 +62,7 @@ name="month" hx-get="{% url 'work-records-change-month' %}" hx-target="#workRecordTable" - hx-trigger="change" - > + hx-trigger="keyup changed delay:0.5s">
    diff --git a/attendance/templates/attendance_form.html b/attendance/templates/attendance_form.html index 330dc793c..51194a4e9 100644 --- a/attendance/templates/attendance_form.html +++ b/attendance/templates/attendance_form.html @@ -4,7 +4,7 @@ {{ field }} {% endif %} {% endfor %} -
    +
    {{form.non_field_errors}}
    diff --git a/attendance/templates/cbv/attendance_activity/attendance_activity_home.html b/attendance/templates/cbv/attendance_activity/attendance_activity_home.html new file mode 100644 index 000000000..2c0eb254f --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/attendance_activity_home.html @@ -0,0 +1,139 @@ +{% extends "index.html" %} +{% load static %} +{% load i18n %} +{% block content %} + + + +
    +
    + +{% include "generic/components.html" %} + + +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    + +
    + +{% comment %}
    +

    + {% trans "Import Attendance Activities" %} +

    + + +
    + +
    +
    + {% csrf_token %} + + +
    + + +
    + +
    +
    +
    +
    {% endcomment %} + + +{% comment %} {% endcomment %} + + +{% endblock content %} \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_activity/attendance_export.html b/attendance/templates/cbv/attendance_activity/attendance_export.html new file mode 100644 index 000000000..bb98c1fec --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/attendance_export.html @@ -0,0 +1,31 @@ +{% load i18n %} +{% load basefilters %} + +
    +
    +

    + {% trans "Export Attendances" %} +

    + +
    +
    + {% csrf_token %} {% include 'attendance/attendance_activity/export_filter.html'%} + +
    +
    +
    +
    diff --git a/attendance/templates/cbv/attendance_activity/delete_action.html b/attendance/templates/cbv/attendance_activity/delete_action.html new file mode 100644 index 000000000..271b30d09 --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/delete_action.html @@ -0,0 +1,13 @@ +{% load i18n %} + +
    + {% csrf_token %} + +
    + + + diff --git a/attendance/templates/cbv/attendance_activity/delete_inherit.html b/attendance/templates/cbv/attendance_activity/delete_inherit.html new file mode 100644 index 000000000..11c13c6fe --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/delete_inherit.html @@ -0,0 +1,8 @@ +
    + {% include 'generic/horilla_list_table.html' %} +
    +{% if request.GET.deleted %} + +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_activity/detail_delete_action.html b/attendance/templates/cbv/attendance_activity/detail_delete_action.html new file mode 100644 index 000000000..039c46121 --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/detail_delete_action.html @@ -0,0 +1,20 @@ +{% load i18n %} +{% if perms.attendance.delete_attendanceactivity %} +
    + +
    +{% endif %} +{% if request.GET.deleted %} + +{% endif %} + diff --git a/attendance/templates/cbv/attendance_activity/filter.html b/attendance/templates/cbv/attendance_activity/filter.html new file mode 100644 index 000000000..b75a994a9 --- /dev/null +++ b/attendance/templates/cbv/attendance_activity/filter.html @@ -0,0 +1,108 @@ +{% load static %} {% load i18n %} +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{form.employee_id}} +
    +
    + + {{form.employee_id__employee_work_info__department_id}} +
    +
    + + {{form.employee_id__employee_work_info__shift_id}} +
    +
    + + {{form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{form.employee_id__employee_work_info__company_id}} +
    +
    + + {{form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{form.employee_id__employee_work_info__work_type_id}} +
    +
    + + {{form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    +
    +
    {% trans "Attendance Activity" %}
    +
    +
    +
    +
    + + {{form.attendance_date}} +
    +
    + + {{form.clock_out_date}} +
    +
    +
    +
    + + {{form.clock_in_date}} +
    +
    + + {{form.shift_day}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.attendance_date_from}} +
    +
    + + {{form.in_from}} +
    +
    + + {{form.out_from}} +
    +
    +
    +
    + + {{form.attendance_date_till}} +
    +
    + + {{form.in_till}} +
    +
    + + {{form.out_till}} +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_request/attendance_request.html b/attendance/templates/cbv/attendance_request/attendance_request.html new file mode 100644 index 000000000..c638263bf --- /dev/null +++ b/attendance/templates/cbv/attendance_request/attendance_request.html @@ -0,0 +1,138 @@ + + + +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + +
    +
    +
    +
    + + + +
    +
    +
    + + + + + + + + + + + +{% endblock content %} \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_request/attendance_request_tab.html b/attendance/templates/cbv/attendance_request/attendance_request_tab.html new file mode 100644 index 000000000..432f9b031 --- /dev/null +++ b/attendance/templates/cbv/attendance_request/attendance_request_tab.html @@ -0,0 +1,822 @@ +{% load static i18n generic_template_filters %} +
    + + {% include "generic/export_fields_modal.html" %} + + + {% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %} + {% if queryset|length %} + {% if bulk_select_option %} + {% include "generic/quick_actions.html" %} + {% endif %} + + +
    +
    +
    + +
    + {{ toggle_form.as_list }} +
    +
    +
    + +
    + + + + {% if bulk_select_option %} + + {% endif %} + {% for cell in columns %} + {% with cell_attr=header_attrs|get_item:cell.1 %} + + {% endwith %} + {% endfor %} + {% if options or option_method%} + + {% endif %} + {% if actions or action_method %} + + {% endif %} + + + + {% for instance in queryset %} + + {% if bulk_select_option %} + + {% endif %} + {% for cell in columns %} + {% with attribute=cell.1 index=forloop.counter %} + + {% endwith %} + {% endfor %} + {% if options or option_method %} + + {% endif %} + {% if actions or action_method %} + + {% endif %} + + {% endfor %} + +
    +
    + +
    +
    +
    + {{cell.0}} +
    +
    + {% trans "Options" %} + + {% trans "Actions" %} +
    +
    + +
    +
    + {% if not cell.2 %} + {{instance|getattribute:attribute|selected_format:request.user.employee_get.employee_work_info.company_id|safe}} + {% else %} +
    +
    + +
    + + {{instance|getattribute:attribute}} + +
    + {% endif %} +
    + {% if not option_method %} +
    + {% for option in options %} + {% if option.accessibility|accessibility:instance %} + + + + {% endif %} + {% endfor %} +
    + {% else %} {{instance|getattribute:option_method|safe}} {% endif %} +
    + {% if not action_method %} +
    + {% for action in actions %} + {% if action.accessibility|accessibility:instance %} + + {% endif %} + {% endfor %} +
    + {% else %} {{instance|getattribute:action_method|safe}} {% endif %} +
    +
    +
    + {% if queryset.paginator.count %} +
    + {% trans "Page" %} {{queryset.number}} {% trans "of" %} + {{queryset.paginator.num_pages}} + + +
    + + {% if bulk_select_option %} + + {% endif %} + {% endif %} + + + {% else %} + {% if row_status_indications %} +
    + {% for indication in row_status_indications %} + + + {{indication.1}} + + {% endfor %} +
    + {% endif %} +
    +
    + Page not found. 404. +

    {% trans "No Records found" %}

    +

    + {% trans "No records found." %} +

    +
    +
    + {% endif %} +
    + +{% comment %} + {% load static i18n generic_template_filters %} +
    + + + {% include "generic/export_fields_modal.html" %} + + + {% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %} + {% if queryset|length %} + {% if bulk_select_option %} + {% include "generic/quick_actions.html" %} + {% endif %} + + +
    +
    +
    + +
    + {{toggle_form.as_list}} +
    +
    +
    +
    +
    +
    +
    + {% if bulk_select_option %} +
    +
    + +
    +
    + {% endif %} {% for cell in columns %} +
    +
    + {{cell.0}} +
    +
    + {% endfor %} {% if options or option_method%} +
    +
    + {% trans "Options" %} +
    +
    + {% endif %} {% if actions or action_method %} +
    +
    + {% trans "Actions" %} +
    +
    + {% endif %} +
    +
    +
    + {% for instance in queryset %} +
    + {% if bulk_select_option %} +
    +
    + +
    +
    + {% endif %} {% for cell in columns %} + {% with attribute=cell.1 index=forloop.counter %} {% if not cell.2 %} + +
    + {{instance|getattribute:attribute|selected_format:request.user.employee_get.employee_work_info.company_id|safe}} +
    + {% else %} +
    +
    +
    + +
    + + {{instance|getattribute:attribute}} + +
    +
    + {% endif %} {% endwith %} {% endfor %} {% if options or option_method %} +
    + {% if not option_method %} +
    + {% for option in options %} + {% if option.accessibility|accessibility:instance %} + + + + {% endif %} + {% endfor %} +
    + {% else %} {{instance|getattribute:option_method|safe}} {% endif %} +
    + {% endif %} {% if actions or action_method %} +
    + {% if not action_method %} +
    + {% for action in actions %} + {% if action.accessibility|accessibility:instance %} + + + + {% endif %} + {% endfor %} +
    + {% else %} {{instance|getattribute:action_method|safe}} {% endif %} +
    + {% endif %} +
    + {% endfor %} +
    +
    +
    +
    + {% if queryset.paginator.count %} +
    + {% trans "Page" %} {{queryset.number}} {% trans "of" %} + {{queryset.paginator.num_pages}} + + +
    + + {% if bulk_select_option %} + + {% endif %} + {% endif %} + + {% else %} + {% if row_status_indications %} +
    + {% for indication in row_status_indications %} + + + {{indication.1}} + + {% endfor %} +
    + {% endif %} +
    +
    + Page not found. 404. +

    {% trans "No Records found" %}

    +

    + {% trans "No records found." %} +

    +
    +
    + {% endif %} +
    {% endcomment %} + + diff --git a/attendance/templates/cbv/attendance_request/comment.html b/attendance/templates/cbv/attendance_request/comment.html new file mode 100644 index 000000000..011325a1b --- /dev/null +++ b/attendance/templates/cbv/attendance_request/comment.html @@ -0,0 +1,7 @@ +{% load basefilters %}{% load i18n %} +
    + +
    \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_request/request_actions.html b/attendance/templates/cbv/attendance_request/request_actions.html new file mode 100644 index 000000000..2bca2c385 --- /dev/null +++ b/attendance/templates/cbv/attendance_request/request_actions.html @@ -0,0 +1,18 @@ +{% load i18n %} +
    + + + + + +
    \ No newline at end of file diff --git a/attendance/templates/cbv/attendance_request/status.html b/attendance/templates/cbv/attendance_request/status.html new file mode 100644 index 000000000..c8b8f31e0 --- /dev/null +++ b/attendance/templates/cbv/attendance_request/status.html @@ -0,0 +1,11 @@ +{% load i18n %} +{% if instance.request_type == "create_request" %} + + {% trans "Created Request" %} +{% elif instance.request_type == "update_request" %} + + {% trans "Update Request" %} +{% elif instance.request_type == "revalidate_request" %} + + {% trans "Re-validate Request" %} +{% endif %} diff --git a/attendance/templates/cbv/attendances/attendance_actions.html b/attendance/templates/cbv/attendances/attendance_actions.html new file mode 100644 index 000000000..53a5312a6 --- /dev/null +++ b/attendance/templates/cbv/attendances/attendance_actions.html @@ -0,0 +1,35 @@ +{% load i18n %} +{% load static %} +{% load basefilters %} + +
    + {% if perms.instance.change_attendance or request.user|is_reportingmanager %} + + + + {% endif %} + {% if perms.instance.delete_attendance or request.user|is_reportingmanager %} +
    + {% csrf_token %} + +
    + {% endif %} +
    diff --git a/attendance/templates/cbv/attendances/attendance_view_page.html b/attendance/templates/cbv/attendances/attendance_view_page.html new file mode 100644 index 000000000..62e5feff6 --- /dev/null +++ b/attendance/templates/cbv/attendances/attendance_view_page.html @@ -0,0 +1,86 @@ +{% extends "index.html" %} +{% load static %} +{% load i18n %} + +{% block content %} + + + +
    +
    + +{% include "generic/components.html" %} + + + + +
    +
    +
    + + + + + + + + + + + + +{% endblock content %} diff --git a/attendance/templates/cbv/attendances/attendances_export_page.html b/attendance/templates/cbv/attendances/attendances_export_page.html new file mode 100644 index 000000000..db048c371 --- /dev/null +++ b/attendance/templates/cbv/attendances/attendances_export_page.html @@ -0,0 +1,187 @@ +{% load static %} {% load i18n %} +
    +

    + {% trans "Export Attendances" %} +

    + +
    +
    +
    + {% csrf_token %} +
    +
    +
    {% trans "Excel columns" %}
    +
    +
    +
    +
    + +
    +
    +
    +
    + {% for field in export_form.selected_fields %} +
    +
    + +
    +
    + {% endfor %} +
    +
    +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{export.form.employee_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__department_id}} +
    +
    + + {{export.form.shift_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{export.form.employee_id__employee_work_info__company_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{export.form.work_type_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    +
    +
    {% trans "Attendance" %}
    +
    +
    +
    +
    + + {{export.form.attendance_date}} +
    +
    + + {{export.form.attendance_clock_in}} +
    +
    + + {{export.form.attendance_validated}} +
    +
    +
    +
    + + {{export.form.minimum_hour}} +
    +
    + + {{export.form.attendance_clock_out}} +
    +
    + + {{export.form.attendance_overtime_approve}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{export.form.attendance_date__gte}} +
    +
    + + {{export.form.attendance_clock_in__gte}} +
    +
    + + {{export.form.attendance_clock_out__gte}} +
    +
    + + {{export.form.at_work_second__gte}} +
    +
    + + {{export.form.overtime_second__gte}} +
    +
    +
    +
    + + {{export.form.attendance_date__lte}} +
    +
    + + {{export.form.attendance_clock_in__lte}} +
    +
    + + {{export.form.attendance_clock_out__lte}} +
    +
    + + {{export.form.at_work_second__lte}} +
    +
    + + {{export.form.overtime_second__lte}} +
    +
    +
    +
    +
    +
    + +
    +
    diff --git a/attendance/templates/cbv/attendances/attendances_filter_page.html b/attendance/templates/cbv/attendances/attendances_filter_page.html new file mode 100644 index 000000000..46a30530a --- /dev/null +++ b/attendance/templates/cbv/attendances/attendances_filter_page.html @@ -0,0 +1,158 @@ +{% load static %} {% load i18n %} +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{form.employee_id}} +
    +
    + + {{form.employee_id__employee_work_info__department_id}} +
    +
    + + {{form.shift_id}} +
    +
    + + {{form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{form.employee_id__employee_work_info__company_id}} +
    +
    + + {{form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{form.work_type_id}} +
    +
    + + {{form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    + {% comment %} {{form.}} {% endcomment %} +
    +
    {% trans "Attendance" %}
    +
    +
    +
    +
    + + {{form.attendance_date}} +
    +
    + + {{form.attendance_clock_in}} +
    +
    + + {{form.attendance_validated}} +
    +
    +
    +
    + + {{form.minimum_hour}} +
    +
    + + {{form.attendance_clock_out}} +
    +
    + + {{form.attendance_overtime_approve}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.attendance_date__gte}} +
    +
    + + {{form.attendance_clock_in__gte}} +
    +
    + + {{form.attendance_clock_out__gte}} +
    +
    + + {{form.at_work_second__gte}} +
    +
    + + {{form.pending_hour__gte}} +
    +
    + + {{form.overtime_second__gte}} +
    +
    +
    +
    + + {{form.attendance_date__lte}} +
    +
    + + {{form.attendance_clock_in__lte}} +
    +
    + + {{form.attendance_clock_out__lte}} +
    +
    + + {{form.at_work_second__lte}} +
    +
    + + {{form.pending_hour__lte}} +
    +
    + + {{form.overtime_second__lte}} +
    + +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/attendance/templates/cbv/attendances/detail_view_activity_col.html b/attendance/templates/cbv/attendances/detail_view_activity_col.html new file mode 100644 index 000000000..2a5b7554d --- /dev/null +++ b/attendance/templates/cbv/attendances/detail_view_activity_col.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% include "generic/components.html" %} + {% trans "Activities" %} + +

    {{instance.activities.count}} {% trans "Activity" %}

    + +
    diff --git a/attendance/templates/cbv/attendances/ot_confirmation.html b/attendance/templates/cbv/attendances/ot_confirmation.html new file mode 100644 index 000000000..173c4ffb7 --- /dev/null +++ b/attendance/templates/cbv/attendances/ot_confirmation.html @@ -0,0 +1,49 @@ +{% load i18n %} +
    + {% if instance.attendance_overtime_approve %} + + + + {% elif minot <= instance.overtime_second %} + + + + {% elif instance.overtime_second >= 60 %} + + + + + {% endif %} +
    diff --git a/attendance/templates/cbv/attendances/ot_tab_action.html b/attendance/templates/cbv/attendances/ot_tab_action.html new file mode 100644 index 000000000..b4958731d --- /dev/null +++ b/attendance/templates/cbv/attendances/ot_tab_action.html @@ -0,0 +1,94 @@ +{% load i18n %} +{% load static %} +{% load basefilters %} + +{% if perms.instance.change_attendance and perms.instance.delete_attendance or request.user|is_reportingmanager %} + + + + + {% if instance.attendance_overtime_approve %} + + + + {% elif minot <= instance.overtime_second %} + + + + {% elif instance.overtime_second >= 60 %} + + + + + {% endif %} + + {% comment %} {% if instance.attendance_overtime_approve %} + + + + {% else %} + + + + {% endif %} {% endcomment %} + +
    + {% csrf_token %} + +
    +{% endif %} diff --git a/attendance/templates/cbv/attendances/validate_button.html b/attendance/templates/cbv/attendances/validate_button.html new file mode 100644 index 000000000..dfc152460 --- /dev/null +++ b/attendance/templates/cbv/attendances/validate_button.html @@ -0,0 +1,17 @@ +{% load i18n %} +{% load basefilters %} +{% if request.user.employee_get != instance.employee_id and request.user|is_reportingmanager or perms.attendance.change_attendance or request.user|is_self_reporting_manager %} + +{% trans "Validate" %} +{% else %} + +{% trans "Validate" %} + + +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/attendances/validate_tab_action.html b/attendance/templates/cbv/attendances/validate_tab_action.html new file mode 100644 index 000000000..90c35a9bb --- /dev/null +++ b/attendance/templates/cbv/attendances/validate_tab_action.html @@ -0,0 +1,48 @@ +{% load i18n %} +{% load static %} +{% load basefilters %} + +{% if perms.attendance.change_attendance and perms.attendance.delete_attendance or request.user|is_reportingmanager %} + + + + + {% comment %} {% if request.user.employee_get != instance.employee_id and request.user|is_reportingmanager or perms.attendance.change_attendance or request.user|is_self_reporting_manager %} {% endcomment %} + + + + + +{% comment %} {% if perms.attendance.delete_attendance %} {% endcomment %} +
    + {% csrf_token %} + +
    +{% endif %} diff --git a/attendance/templates/cbv/attendances/validated_tab_action.html b/attendance/templates/cbv/attendances/validated_tab_action.html new file mode 100644 index 000000000..fd591ec2b --- /dev/null +++ b/attendance/templates/cbv/attendances/validated_tab_action.html @@ -0,0 +1,28 @@ +{% load i18n %} +{% load static %} +{% load basefilters %} + +{% if perms.instance.change_attendance and perms.instance.delete_attendance or request.user|is_reportingmanager %} + + + +
    + {% csrf_token %} + +
    +{% endif %} diff --git a/attendance/templates/cbv/hour_account/hour_account.html b/attendance/templates/cbv/hour_account/hour_account.html new file mode 100644 index 000000000..02dc7b3c2 --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_account.html @@ -0,0 +1,166 @@ + + + +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + + + + + + +
    +
    + + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + +
    +
    +
    + + +
    + + + +{% endblock content %} \ No newline at end of file diff --git a/attendance/templates/cbv/hour_account/hour_account_main.html b/attendance/templates/cbv/hour_account/hour_account_main.html new file mode 100644 index 000000000..6ce9327b1 --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_account_main.html @@ -0,0 +1,8 @@ +
    + {% include "generic/horilla_list.html" %} +
    +{% if request.GET.deleted %} + +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/hour_account/hour_actions.html b/attendance/templates/cbv/hour_account/hour_actions.html new file mode 100644 index 000000000..4de9247ea --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_actions.html @@ -0,0 +1,23 @@ +{% load i18n %}{% load basefilters%} + +{% if perms.attendance.change_attendanceovertime or perms.attendance.delete_attendanceovertime %} +
    +
    + {% if perms.attendance.change_attendanceovertime or request.user|is_reportingmanager %} + + {% endif %} + {% if perms.attendance.delete_attendanceovertime or request.user|is_reportingmanager %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    +{% endif %} + + + + \ No newline at end of file diff --git a/attendance/templates/cbv/hour_account/hour_detail_action.html b/attendance/templates/cbv/hour_account/hour_detail_action.html new file mode 100644 index 000000000..06e2fcd0a --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_detail_action.html @@ -0,0 +1,78 @@ + +{% load i18n %} + {% load basefilters %} + {% with dates=instance.month_days %} + {% if perms.attendance.view_attendance or request.user|is_reportingmanager %} + + + + + {% else %} + + + + {% endif %} + + {% if perms.attendance.view_attendance or request.user|is_reportingmanager %} + + + + {% else %} + + + + + {% endif %} + {% endwith %} + + + + + + + + \ No newline at end of file diff --git a/attendance/templates/cbv/hour_account/hour_export.html b/attendance/templates/cbv/hour_account/hour_export.html new file mode 100644 index 000000000..6feb23f0b --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_export.html @@ -0,0 +1,148 @@ +{% load i18n %} +
    +
    +
    {% trans "Excel columns" %}
    +
    +
    +
    +
    + +
    +
    +
    +
    + {% for field in export_fields.selected_fields %} +
    +
    + +
    +
    + {% endfor %} +
    +
    +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{export_obj.form.employee_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__department_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__shift_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{export_obj.form.employee_id__employee_work_info__company_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__work_type_id}} +
    +
    + + {{export_obj.form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    +
    +
    {% trans "Worked Hours" %}
    +
    +
    +
    +
    + + {{export_obj.form.month}} +
    +
    + + {{export_obj.form.overtime}} +
    +
    +
    +
    + + {{export_obj.form.year}} +
    +
    + + {{export_obj.form.worked_hours}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{export_obj.form.worked_hours__gte}} +
    +
    + + {{export_obj.form.pending_hours__gte}} +
    +
    + + {{export_obj.form.overtime__gte}} +
    +
    +
    +
    + + {{export_obj.form.worked_hours__lte}} +
    +
    + + {{export_obj.form.pending_hours__lte}} +
    +
    + + {{export_obj.form.overtime__lte}} +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/attendance/templates/cbv/hour_account/hour_filter.html b/attendance/templates/cbv/hour_account/hour_filter.html new file mode 100644 index 000000000..b461e5cbb --- /dev/null +++ b/attendance/templates/cbv/hour_account/hour_filter.html @@ -0,0 +1,121 @@ + +{% load static %} {% load i18n %} +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{form.employee_id}} +
    +
    + + {{form.employee_id__employee_work_info__department_id}} +
    +
    + + {{form.employee_id__employee_work_info__shift_id}} +
    +
    + + {{form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{form.employee_id__employee_work_info__company_id}} +
    +
    + + {{form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{form.employee_id__employee_work_info__work_type_id}} +
    +
    + + {{form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    +
    +
    {% trans "Worked Hours" %}
    +
    +
    +
    +
    + + {{form.month}} +
    +
    + + {{form.overtime}} +
    +
    +
    +
    + + {{form.year}} +
    +
    + + {{form.worked_hours}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.worked_hours__gte}} +
    +
    + + {{form.pending_hours__gte}} +
    +
    + + {{form.overtime__gte}} +
    +
    +
    +
    + + {{form.worked_hours__lte}} +
    +
    + + {{form.pending_hours__lte}} +
    +
    + + {{form.overtime__lte}} +
    +
    +
    +
    +
    +
    diff --git a/attendance/templates/cbv/hour_account/nav_hour_account.html b/attendance/templates/cbv/hour_account/nav_hour_account.html new file mode 100644 index 000000000..eb46c58ca --- /dev/null +++ b/attendance/templates/cbv/hour_account/nav_hour_account.html @@ -0,0 +1,11 @@ +
    + {% include 'generic/horilla_nav.html' %} +
    + + \ No newline at end of file diff --git a/attendance/templates/cbv/late_come_and_early_out/actions_column.html b/attendance/templates/cbv/late_come_and_early_out/actions_column.html new file mode 100644 index 000000000..21e6d6c45 --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/actions_column.html @@ -0,0 +1,33 @@ +{% load i18n %}{% load basefilters %} + +{% if request.user|is_reportingmanager or perms.attendance.chanage_penaltyaccount or perms.attendance.delete_attendancelatecomeearlyout %} +
    +
    + {% if request.user|is_reportingmanager or perms.attendance.chanage_penaltyaccount %} + + {% endif %} + {% if perms.attendance.delete_attendancelatecomeearlyout %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    + {% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/late_come_and_early_out/detail_action.html b/attendance/templates/cbv/late_come_and_early_out/detail_action.html new file mode 100644 index 000000000..bf1ab8a0f --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/detail_action.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% load basefilters %} + +{% if request.user|is_reportingmanager or perms.attendance.chanage_penaltyaccount or perms.attendance.delete_attendancelatecomeearlyout %} + {% if request.user|is_reportingmanager or perms.attendance.chanage_penaltyaccount %} + + + + {% trans "Penalty" %} + + {% endif %} + {% if perms.attendance.delete_attendancelatecomeearlyout %} +
    + {% csrf_token %} + +
    + {% endif %} + + {% endif %} + +{% if request.GET.deleted %} + +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/late_come_and_early_out/late_come_and_early_out.html b/attendance/templates/cbv/late_come_and_early_out/late_come_and_early_out.html new file mode 100644 index 000000000..c974c91c6 --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/late_come_and_early_out.html @@ -0,0 +1,150 @@ + + + +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + + + + + +{% endblock content %} \ No newline at end of file diff --git a/attendance/templates/cbv/late_come_and_early_out/late_early_export.html b/attendance/templates/cbv/late_come_and_early_out/late_early_export.html new file mode 100644 index 000000000..419465037 --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/late_early_export.html @@ -0,0 +1,168 @@ +{% load static %} {% load i18n %} +
    +
    +
    {% trans "Excel columns" %}
    +
    +
    +
    +
    + +
    +
    +
    +
    + {% for field in export_form.selected_fields %} +
    +
    + +
    +
    + {% endfor %} +
    +
    +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{export.form.employee_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__department_id}} +
    +
    + + {{export.form.attendance_id__shift_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__reporting_manager_id}} +
    +
    +
    +
    + + {{export.form.employee_id__employee_work_info__company_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{export.form.attendance_id__work_type_id}} +
    +
    + + {{export.form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    +
    +
    {% trans "Late Come/Early Out" %}
    +
    +
    +
    +
    + + {{export.form.type}} +
    +
    + + {{export.form.attendance_date}} +
    +
    + + {{export.form.attendance_clock_out}} +
    +
    + + {{export.form.attendance_id__attendance_validated}} +
    +
    +
    +
    + + {{export.form.attendance_id__minimum_hour}} +
    +
    + + {{export.form.attendance_clock_in}} +
    +
    + + {{export.form.attendance_id__attendance_overtime_approve}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{export.form.attendance_date__gte}} +
    +
    + + {{export.form.attendance_clock_in__gte}} +
    +
    + + {{export.form.attendance_clock_out__gte}} +
    +
    + + {{export.form.at_work_second__gte}} +
    +
    + + {{export.form.overtime_second__gte}} +
    +
    +
    +
    + + {{export.form.attendance_date__lte}} +
    +
    + + {{export.form.attendance_clock_in__lte}} +
    +
    + + {{export.form.attendance_clock_out__lte}} +
    +
    + + {{export.form.at_work_second__lte}} +
    +
    + + {{export.form.overtime_second__lte}} +
    +
    +
    +
    +
    +
    + diff --git a/attendance/templates/cbv/late_come_and_early_out/late_early_filter.html b/attendance/templates/cbv/late_come_and_early_out/late_early_filter.html new file mode 100644 index 000000000..3ac39e13f --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/late_early_filter.html @@ -0,0 +1,145 @@ +{% load static %} +{% load i18n %} +
    +
    +
    {% trans "Work Info" %}
    +
    +
    +
    +
    + + {{form.employee_id}} +
    +
    + + {{form.employee_id__employee_work_info__department_id}} +
    +
    + + {{form.attendance_id__shift_id}} +
    +
    + + {{form.employee_id__employee_work_info__reporting_manager_id}} +
    + + +
    +
    +
    + + {{form.employee_id__employee_work_info__company_id}} +
    +
    + + {{form.employee_id__employee_work_info__job_position_id}} +
    +
    + + {{form.attendance_id__work_type_id}} +
    +
    + + {{form.employee_id__employee_work_info__location}} +
    +
    +
    +
    +
    + {% comment %} {{f.form}} {% endcomment %} +
    +
    {% trans "Late Come/Early Out" %}
    +
    +
    +
    +
    + + {{form.type}} +
    +
    + + {{form.attendance_date}} +
    +
    + + {{form.attendance_clock_out}} +
    +
    + + {{form.attendance_id__attendance_validated}} +
    +
    +
    +
    + + {{form.attendance_id__minimum_hour}} +
    +
    + + {{form.attendance_clock_in}} +
    +
    + + {{form.attendance_id__attendance_overtime_approve}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.attendance_date__gte}} +
    +
    + + {{form.attendance_clock_in__gte}} +
    +
    + + {{form.attendance_clock_out__gte}} +
    +
    + + {{form.at_work_second__gte}} +
    +
    + + {{form.overtime_second__gte}} +
    +
    +
    +
    + + {{form.attendance_date__lte}} +
    +
    + + {{form.attendance_clock_in__lte}} +
    +
    + + {{form.attendance_clock_out__lte}} +
    +
    + + {{form.at_work_second__lte}} +
    +
    + + {{form.overtime_second__lte}} +
    +
    +
    +
    +
    +
    + + + + + diff --git a/attendance/templates/cbv/late_come_and_early_out/penality.html b/attendance/templates/cbv/late_come_and_early_out/penality.html new file mode 100644 index 000000000..8830f2253 --- /dev/null +++ b/attendance/templates/cbv/late_come_and_early_out/penality.html @@ -0,0 +1,7 @@ + + +{% if instance.get_penalties_count %} +
    Penalties :{{instance.get_penalties_count}}
    +{% else %} +
    No Penalties
    +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/my_attendances/my_attendance_filter.html b/attendance/templates/cbv/my_attendances/my_attendance_filter.html new file mode 100644 index 000000000..286d461a8 --- /dev/null +++ b/attendance/templates/cbv/my_attendances/my_attendance_filter.html @@ -0,0 +1,97 @@ + +{% load i18n %} {% load basefilters %} +
    +
    +
    {% trans "Attendance" %}
    +
    +
    +
    +
    + + {{form.attendance_date}} +
    +
    + + {{form.attendance_clock_in}} +
    +
    + + {{form.attendance_validated}} +
    +
    + + {{form.is_validate_request}} +
    +
    +
    +
    + + {{form.minimum_hour}} +
    +
    + + {{form.attendance_clock_out}} +
    +
    + + {{form.attendance_overtime_approve}} +
    +
    + + {{form.is_validate_request_approved}} +
    +
    +
    +
    +
    +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.attendance_date__gte}} +
    +
    + + {{form.attendance_clock_in__lte}} +
    +
    + + {{form.attendance_clock_out__lte}} +
    +
    + + {{form.at_work_second__gte}} +
    +
    + + {{form.overtime_second__gte}} +
    +
    +
    +
    + + {{form.attendance_date__lte}} +
    +
    + + {{form.attendance_clock_in__lte}} +
    +
    + + {{form.attendance_clock_out__lte}} +
    +
    + + {{form.at_work_second__lte}} +
    +
    + + {{form.overtime_second__lte}} +
    +
    +
    +
    +
    diff --git a/attendance/templates/cbv/my_attendances/my_attendances.html b/attendance/templates/cbv/my_attendances/my_attendances.html new file mode 100644 index 000000000..3566d4f0d --- /dev/null +++ b/attendance/templates/cbv/my_attendances/my_attendances.html @@ -0,0 +1,99 @@ + + + +{% extends "index.html" %} +{% load i18n %}{% load static %} + + +{% block content %} + + + + + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + +
    +
    +
    + + + + + + +{% endblock content %} \ No newline at end of file diff --git a/attendance/templates/cbv/settings/applicable_on_clock_in_col.html b/attendance/templates/cbv/settings/applicable_on_clock_in_col.html new file mode 100644 index 000000000..f1ca0c4ed --- /dev/null +++ b/attendance/templates/cbv/settings/applicable_on_clock_in_col.html @@ -0,0 +1,7 @@ +
    + {% if perms.attendance.change_gracetime %} + + {% else %} + + {% endif %} +
    \ No newline at end of file diff --git a/attendance/templates/cbv/settings/applicable_on_clock_out_col.html b/attendance/templates/cbv/settings/applicable_on_clock_out_col.html new file mode 100644 index 000000000..1639b3482 --- /dev/null +++ b/attendance/templates/cbv/settings/applicable_on_clock_out_col.html @@ -0,0 +1,7 @@ +
    + {% if perms.attendance.change_gracetime %} + + {% else %} + + {% endif %} +
    \ No newline at end of file diff --git a/attendance/templates/cbv/settings/break_point_action.html b/attendance/templates/cbv/settings/break_point_action.html new file mode 100644 index 000000000..931dba837 --- /dev/null +++ b/attendance/templates/cbv/settings/break_point_action.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% load static %} +{% if perms.attendance.change_attendancevalidationcondition %} + {% trans 'Edit' %} + +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/settings/check_in_check_out_col.html b/attendance/templates/cbv/settings/check_in_check_out_col.html new file mode 100644 index 000000000..5fe97eb05 --- /dev/null +++ b/attendance/templates/cbv/settings/check_in_check_out_col.html @@ -0,0 +1,21 @@ +{% load i18n %} +
    + + +
    \ No newline at end of file diff --git a/attendance/templates/cbv/settings/grace_time_default_action.html b/attendance/templates/cbv/settings/grace_time_default_action.html new file mode 100644 index 000000000..84cc5b1dc --- /dev/null +++ b/attendance/templates/cbv/settings/grace_time_default_action.html @@ -0,0 +1,40 @@ +{% load i18n %} + +{% if perms.attendance.change_gracetime%} +
    + {% if perms.base.change_employeeshift %} + + + + {% endif %} + {% if perms.base.change_gracetime %} + + + + {% endif %} + {% if perms.base.delete_gracetime %} + + {% else %} + + {% endif %} +
    +{% endif %} diff --git a/attendance/templates/cbv/settings/grace_time_shift.html b/attendance/templates/cbv/settings/grace_time_shift.html new file mode 100644 index 000000000..f665a7b7a --- /dev/null +++ b/attendance/templates/cbv/settings/grace_time_shift.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% if instance.employee_shift.all %} +{% for shift in instance.employee_shift.all %} + {{shift}}
    +{% endfor %} +{% else %} +{% trans "Nil" %} +{% endif %} \ No newline at end of file diff --git a/attendance/templates/cbv/settings/is_active_col_grace_time.html b/attendance/templates/cbv/settings/is_active_col_grace_time.html new file mode 100644 index 000000000..b2aa3ce09 --- /dev/null +++ b/attendance/templates/cbv/settings/is_active_col_grace_time.html @@ -0,0 +1,11 @@ + +{% load i18n %} +{% if perms.attendance.change_gracetime %} +
    + {% if perms.attendance.change_gracetime %} + + {% else %} + + {% endif %} +
    +{% endif %} \ No newline at end of file diff --git a/attendance/templates/requests/attendance/form_field.html b/attendance/templates/requests/attendance/form_field.html index 662e0ac4d..aae4f68c2 100644 --- a/attendance/templates/requests/attendance/form_field.html +++ b/attendance/templates/requests/attendance/form_field.html @@ -1,6 +1,23 @@ {% load i18n %} - -{{field}} +{% load widget_tweaks %} +{% comment %} +{{field}} {% endcomment %} +
    + + {% if field.help_text != '' %} + + {% endif %} +
    +
    + {{ field|add_class:'form-control' }} + {{ field.errors }} +
    + ") elif hx_target: @@ -773,6 +784,50 @@ def attendance_account_bulk_delete(request): return JsonResponse({"message": "Success"}) +@login_required +def form_shift_dynamic_data(request): + """ + This method is used to update the shift details to the form + """ + shift_id = request.POST["shift_id"] + attendance_date_str = request.POST.get("attendance_date") + today = datetime.now() + attendance_date = date(day=today.day, month=today.month, year=today.year) + if attendance_date_str is not None and attendance_date_str != "": + attendance_date = datetime.strptime(attendance_date_str, "%Y-%m-%d").date() + day = attendance_date.strftime("%A").lower() + schedule_today = EmployeeShiftSchedule.objects.filter( + shift_id__id=shift_id, day__day=day + ).first() + shift_start_time = "" + shift_end_time = "" + minimum_hour = "00:00" + attendance_clock_out_date = attendance_date + if schedule_today is not None: + shift_start_time = schedule_today.start_time + shift_end_time = schedule_today.end_time + minimum_hour = schedule_today.minimum_working_hour + if shift_end_time < shift_start_time: + attendance_clock_out_date = attendance_date + timedelta(days=1) + worked_hour = minimum_hour + if attendance_date == date(day=today.day, month=today.month, year=today.year): + shift_end_time = datetime.now().strftime("%H:%M") + worked_hour = "00:00" + + minimum_hour = attendance_day_checking(str(attendance_date), minimum_hour) + + return JsonResponse( + { + "shift_start_time": shift_start_time, + "shift_end_time": shift_end_time, + "checkin_date": attendance_date.strftime("%Y-%m-%d"), + "minimum_hour": minimum_hour, + "worked_hour": worked_hour, + "checkout_date": attendance_clock_out_date.strftime("%Y-%m-%d"), + } + ) + + @login_required def attendance_activity_view(request): """ @@ -846,7 +901,7 @@ def activity_single_view(request, obj_id): def attendance_activity_delete(request, obj_id): """ This method is used to delete attendance activity - args: + args:attendance-activity-delete obj_id : attendance activity id """ request_copy = request.GET.copy() @@ -859,6 +914,7 @@ def attendance_activity_delete(request, obj_id): messages.error(request, _("Attendance activity Does not exists..")) except ProtectedError: messages.error(request, _("You cannot delete this activity")) + if not request.GET.get("instances_ids"): return redirect(f"/attendance/attendance-activity-search?{previous_data}") else: @@ -870,7 +926,7 @@ def attendance_activity_delete(request, obj_id): json.loads(instances_ids), obj_id ) return redirect( - f"/attendance/attendance-activity-single-view/{next_instance}/?{previous_data}&instances_ids={instances_list}" + f"/attendance/attendance-activity-single-view/{next_instance}/?{previous_data}&instance_ids={instances_list}&deleted=true" ) @@ -1212,7 +1268,7 @@ def late_come_early_out_delete(request, obj_id): json.loads(instances_ids), obj_id ) return redirect( - f"/attendance/late-in-early-out-single-view/{next_instance}/?{previous_data}&instances_ids={instances_list}" + f"/attendance/late-in-early-out-single-view/{next_instance}/?{previous_data}&instance_ids={instances_list}&deleted=true" ) @@ -1225,18 +1281,15 @@ def late_come_early_out_bulk_delete(request): """ ids = request.POST["ids"] ids = json.loads(ids) + del_ids = [] for attendance_id in ids: try: late_come = AttendanceLateComeEarlyOut.objects.get(id=attendance_id) late_come.delete() - messages.success( - request, - _("{employee} Late-in early-out deleted.").format( - employee=late_come.employee_id - ), - ) + del_ids.append(late_come) except (AttendanceLateComeEarlyOut.DoesNotExist, OverflowError, ValueError): messages.error(request, _("Attendance not found.")) + messages.success(request, _("{} Late-in early-out deleted.".format(len(del_ids)))) return JsonResponse({"message": "Success"}) @@ -1473,12 +1526,13 @@ def approve_bulk_overtime(request): """ ids = request.POST["ids"] ids = json.loads(ids) + otapprove_ids = [] for attendance_id in ids: try: attendance = Attendance.objects.get(id=attendance_id) attendance.attendance_overtime_approve = True attendance.save() - messages.success(request, _("Overtime approved")) + otapprove_ids.append(attendance) notify.send( request.user.employee_get, recipient=attendance.employee_id.employee_user_id, @@ -1497,6 +1551,9 @@ def approve_bulk_overtime(request): ) except (Attendance.DoesNotExist, OverflowError, ValueError): messages.error(request, _("Attendance not found")) + if otapprove_ids: + messages.success(request, _(" {} Overtime approved".format(len(otapprove_ids)))) + return JsonResponse({"message": "Success"}) @@ -1544,14 +1601,20 @@ def update_fields_based_shift(request): employee_ids = ( request.GET.get("employee_id") - if hx_target == "attendanceUpdateForm" or hx_target == "attendanceRequestDiv" + if hx_target == "attendanceUpdate" + or hx_target == "attendanceRequest" + or hx_target == "attendanceUpdateFormFields" + or hx_target == "attendanceFormFields" else request.GET.getlist("employee_id") ) employee_queryset = ( ( Employee.objects.get(id=employee_ids) - if hx_target == "attendanceUpdateForm" + if hx_target == "attendanceUpdate" or hx_target == "attendanceRequestDiv" + or hx_target == "attendanceRequest" + or hx_target == "attendanceUpdateFormFields" + or hx_target == "attendanceFormFields" else Employee.objects.filter(id__in=employee_ids) ) if employee_ids @@ -1609,10 +1672,10 @@ def update_fields_based_shift(request): } form = ( AttendanceUpdateForm(initial=initial_data) - if hx_target == "attendanceUpdateForm" + if hx_target == "attendanceUpdate" or hx_target == "attendanceUpdateFormFields" else ( NewRequestForm(initial=initial_data) - if hx_target == "attendanceRequestDiv" + if hx_target == "attendanceRequest" else AttendanceForm(initial=initial_data) ) ) @@ -1623,6 +1686,41 @@ def update_fields_based_shift(request): ) +@login_required +@hx_request_required +def update_worked_hour_field(request): + """ + Update the worked hour field based on clock-in and clock-out times. + + This view function calculates the total worked hours for an employee + by parsing the clock-in and clock-out dates and times from the request + parameters. It computes the duration between the two times and formats + the result as a string in the "HH:MM" format. The computed worked hours + are then initialized in an AttendanceForm, which is rendered in the + specified HTML template. + """ + clock_in = parse_datetime( + request.GET.get("attendance_clock_in_date"), + request.GET.get("attendance_clock_in"), + ) + clock_out = parse_datetime( + request.GET.get("attendance_clock_out_date"), + request.GET.get("attendance_clock_out"), + ) + + total_seconds = ( + (clock_out - clock_in).total_seconds() if clock_in and clock_out else -1 + ) + hours, minutes = divmod(max(total_seconds, 0), 3600) + worked_hours_str = f"{int(hours):02}:{int(minutes // 60):02}" + form = AttendanceForm(initial={"attendance_worked_hour": worked_hours_str}) + return render( + request, + "attendance/attendance/update_hx_form.html", + {"request": request, "form": form}, + ) + + @login_required @hx_request_required def update_worked_hour_field(request): @@ -1950,6 +2048,7 @@ def create_grace_time(request): @permission_required("base.change_employeeshift") def assign_shift(request, grace_id): gracetime = GraceTime.objects.filter(id=grace_id).first() if grace_id else None + if gracetime: form = GraceTimeAssignForm() if request.method == "POST": @@ -2013,19 +2112,32 @@ def delete_grace_time(request, grace_id): GET : return grace time form template """ try: - GraceTime.objects.get(id=grace_id).delete() + delete_error = False + default_grace_time_count = GraceTime.objects.filter(is_default=True).count() + grace_time_count = GraceTime.objects.filter(is_default=False).count() + grace_time = GraceTime.objects.get(id=grace_id) + grace_time_type = grace_time.is_default + grace_time.delete() messages.success(request, _("Grace time deleted successfully.")) except GraceTime.DoesNotExist: + delete_error = True messages.error(request, _("Grace Time Does not exists..")) except ProtectedError: + delete_error = True messages.error(request, _("Related datas exists.")) - context = { - "condition": AttendanceValidationCondition.objects.first(), - "default_grace_time": GraceTime.objects.filter(is_default=True).first(), - "grace_times": GraceTime.objects.all().exclude(is_default=True), - } - - return render(request, "attendance/grace_time/grace_time_table.html", context) + if delete_error: + if grace_time_type: + return HttpResponse( + "" + ) + return HttpResponse("") + elif default_grace_time_count == 1 and grace_time_type: + return HttpResponse( + "" + ) + elif grace_time_count == 1 and not grace_time_type: + return HttpResponse("") + return HttpResponse("") @login_required diff --git a/base/announcement.py b/base/announcement.py index 6df76eeed..d033b46e8 100644 --- a/base/announcement.py +++ b/base/announcement.py @@ -114,6 +114,7 @@ def create_announcement(request): employee_work_info__job_position_id__in=job_positions ) anou.employees.add(*employees) + anou.save() notify.send( request.user.employee_get, diff --git a/base/cbv/__init__.py b/base/cbv/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/base/cbv/announcement_cbv.py b/base/cbv/announcement_cbv.py new file mode 100644 index 000000000..d7f205b13 --- /dev/null +++ b/base/cbv/announcement_cbv.py @@ -0,0 +1,88 @@ +""" +Announcement page +""" +from django.http import HttpResponse +from base.forms import AnnouncementForm +from base.models import Announcement +from employee.models import Employee +from horilla_views.cbv_methods import login_required, permission_required +from django.utils.decorators import method_decorator +from horilla_views.generic.cbv.views import HorillaFormView +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from django.contrib.auth.models import User +from notifications.signals import notify + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.add_announcement"), name="dispatch" +) +class AnnouncementFormView(HorillaFormView): + """ + form view for create button + """ + + form_class = AnnouncementForm + model = Announcement + new_display_title = _("Create Announcements.") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Edit Announcement.") + + return context + + def form_valid(self, form: AnnouncementForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Announcement updated successfully.") + else: + message = _("Announcement created successfully.") + anou, attachment_ids = form.save(commit=False) + anou.save() + anou.attachments.set(attachment_ids) + employees = form.cleaned_data["employees"] + departments = form.cleaned_data["department"] + job_positions = form.cleaned_data["job_position"] + anou.department.set(departments) + anou.job_position.set(job_positions) + 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( + self.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( + self.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", + ) + messages.success(self.request, message) + return HttpResponse("") + return super().form_valid(form) diff --git a/base/cbv/company.py b/base/cbv/company.py new file mode 100644 index 000000000..2c3710a99 --- /dev/null +++ b/base/cbv/company.py @@ -0,0 +1,207 @@ +""" +this page is handling the cbv methods for company in settings +""" + +from typing import Any +from django import forms +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import CompanyFilter +from base.forms import CompanyForm +from base.models import Company +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.forms import DynamicBulkUpdateForm +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_company"), name="dispatch") +class CompanyListView(HorillaListView): + """ + list view for company in settings + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("company-list") + self.actions = [] + if self.request.user.has_perm("base.change_company"): + self.actions.append( + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + if self.request.user.has_perm("base.delete_company"): + self.actions.append( + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.company&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = Company + filter_class = CompanyFilter + + bulk_template = "cbv/settings/company_bulk_update.html" + bulk_update_fields = ["country", "state", "city", "zip"] + + def get_bulk_form(self): + """ + Bulk from generating method + """ + + form = DynamicBulkUpdateForm( + root_model=Company, bulk_update_fields=self.bulk_update_fields + ) + + form.fields["country"] = forms.ChoiceField( + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2", + "required": True, + "style": "width: 100%; height:45px;", + } + ) + ) + + form.fields["state"] = forms.ChoiceField( + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2", + "required": True, + "style": "width: 100%; height:45px;", + } + ) + ) + + return form + + columns = [ + (_("Company"), "company_icon_with_name"), + (_("Is Hq"), "hq"), + (_("Address"), "address"), + (_("Country"), "country"), + (_("State"), "state"), + (_("City"), "city"), + (_("Zip"), "zip"), + ] + + sortby_mapping = [ + ("Company", "company_icon_with_name"), + ("Country", "country"), + ("State", "state"), + ("City", "city"), + ("Zip", "zip"), + ] + + row_attrs = """ + id="companyTr{get_delete_instance}" + """ + + header_attrs = { + "company_icon_with_name": """ style="width:180px !important" """, + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_company"), name="dispatch") +class CompanyNavView(HorillaNavView): + """ + nav bar of the department view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("company-list") + if self.request.user.has_perm("base.add_company"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('company-create-form')}" + """ + + nav_title = _("Company") + search_swap_target = "#listContainer" + filter_instance = CompanyFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_company"), name="dispatch") +class CompanyCreateForm(HorillaFormView): + """ + form view for creating and editing company in settings + """ + + model = Company + form_class = CompanyForm + template_name = "cbv/settings/company_inherit.html" + new_display_title = _("Create Company") + + def get_form(self, form_class=None): + form = super().get_form(form_class) + + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Company") + + form.fields["country"].widget = forms.Select( + attrs={ + "class": "oh-select oh-select-2", + } + ) + form.fields["state"].widget = forms.Select( + attrs={"class": "oh-select oh-select-2"} + ) + return form + + def form_invalid(self, form: Any) -> HttpResponse: + """ + Handles and renders form errors or defers to superclass. + """ + + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Company") + + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: CompanyForm) -> HttpResponse: + if form.is_valid(): + form.save() + if self.form.instance.pk: + messages.success(self.request, _("Company have been successfully updated.")) + else: + messages.success( + self.request, _("Company have been successfully created.") + ) + return self.HttpResponse() + + return super().form_valid(form) diff --git a/base/cbv/company_leaves.py b/base/cbv/company_leaves.py new file mode 100644 index 000000000..78b28a709 --- /dev/null +++ b/base/cbv/company_leaves.py @@ -0,0 +1,153 @@ +""" +this page is handling the cbv methods of company leaves page +""" + +from typing import Any +from django.http import HttpResponse +from django.contrib import messages +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + TemplateView, + HorillaListView, + HorillaNavView, + HorillaFormView, +) +from horilla_views.cbv_methods import login_required, permission_required +from base.filters import CompanyLeaveFilter +from base.models import CompanyLeaves +from base.forms import CompanyLeaveForm + + +@method_decorator(login_required, name="dispatch") +class CompanyLeavesView(TemplateView): + """ + for page view + """ + + template_name = "cbv/company_leaves/company_leave_home.html" + + +@method_decorator(login_required, name="dispatch") +class CompanyleaveListView(HorillaListView): + """ + list view + """ + + filter_class = CompanyLeaveFilter + model = CompanyLeaves + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("company-leave-list") + self.view_id = "companyleavedelete" + + if self.request.user.has_perm("view_companyleave"): + self.action_method = "company_leave_actions" + + columns = [ + (_("Based On Week"), "custom_based_on_week"), + (_("Based On Week Day"), "based_on_week_day_col"), + ] + + header_attrs = { + "custom_based_on_week": """ + style="width:200px !important;" + """, + + "based_on_week_day_col": """ + style="width:200px !important;" + """, + "action": """ + style="width:200px !important;" + """, + } + + sortby_mapping = [ + ("Based On Week", "custom_based_on_week"), + ("Based On Week Day", "based_on_week_day_col"), + ] + + row_attrs = """ + hx-get='{detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + +@method_decorator(login_required, name="dispatch") +class CompanyLeaveNavView(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("company-leave-list") + if self.request.user.has_perm("add_companyleave"): + self.create_attrs = f""" + hx-get="{reverse_lazy('company-leave-creation')}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + nav_title = _("Company Leaves") + filter_body_template = "cbv/company_leaves/company_leave_filter.html" + filter_form_context_name = "form" + filter_instance = CompanyLeaveFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +class CompanyLeaveDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + model = CompanyLeaves + title = _("Details") + header = { + "title": "get_detail_title", + "subtitle": "", + "avatar": "get_avatar" + } + body = { + (_("Based On Week"), "custom_based_on_week"), + (_("Based On Week Day"), "based_on_week_day_col"), + + } + action_method = "detail_view_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("leave.add_companyleave"),name="dispatch") +class CompanyleaveFormView(HorillaFormView): + """ + form view for create button + """ + + form_class = CompanyLeaveForm + model = CompanyLeaves + new_display_title = _("Create Company Leaves") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Company Leaves") + + return context + + def form_valid(self, form: CompanyLeaveForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Company Leave Updated Successfully") + else: + message = _("New Company Leave Created Successfully") + form.save() + + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/dashboard/dashboard.py b/base/cbv/dashboard/dashboard.py new file mode 100644 index 000000000..7af4c44c5 --- /dev/null +++ b/base/cbv/dashboard/dashboard.py @@ -0,0 +1,257 @@ +""" +This page handles the cbv methods for dashboard views +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from datetime import datetime +from django.utils.decorators import method_decorator +from django.contrib import messages +from base.cbv.work_type_request import WorkRequestListView +from django.utils.translation import gettext_lazy as _ +from base.decorators import manager_can_enter +from base.filters import AnnouncementFilter, AnnouncementViewFilter +from base.methods import filtersubordinates +from base.models import Announcement, AnnouncementView +from employee.filters import EmployeeWorkInformationFilter +from employee.forms import ( + EmployeeWorkInformationUpdateForm, +) +from employee.models import EmployeeWorkInformation +from horilla_views.generic.cbv.views import HorillaFormView, HorillaListView +from horilla_views.cbv_methods import login_required, permission_required +from base.cbv.shift_request import ShiftRequestList + + +@method_decorator(login_required, name="dispatch") +class DashboardWorkTypeRequest(WorkRequestListView): + """ + work type request view in dashboard + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-work-type-request") + self.request.dashboard = "dashboard" + + def get_queryset(self): + """ + queryset to filter data based on permission + """ + queryset = HorillaListView.get_queryset(self) + queryset = queryset.filter( + employee_id__is_active=True, approved=False, canceled=False + ) + queryset = filtersubordinates( + self.request, queryset, "base.add_worktyperequest" + ) + return queryset + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Requested Work Type"), "work_type_id"), + ] + + row_attrs = """ + hx-get='{detail_view}?instance_ids={ordered_ids}&dashboard=true' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + header_attrs = { + "action": """ + style ="width:100px !important" + """, + "employee_id": """ + style ="width:100px !important" + """, + "work_type_id": """ + style ="width:100px !important" + """, + } + + records_per_page = 3 + + option_method = None + row_status_indications = None + + bulk_select_option = False + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestToApprove(ShiftRequestList): + + bulk_select_option = False + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Requested Shift"), "shift_id"), + ] + + header_attrs = { + "action": """ + style ="width:100px !important" + """, + "employee_id": """ + style ="width:100px !important" + """, + "shift_id": """ + style ="width:100px !important" + """, + } + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-shift-request") + + row_attrs = """ + hx-get='{shift_details}?instance_ids={ordered_ids}&dashboard=true' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + records_per_page = 3 + option_method = None + row_status_indications = None + + bulk_select_option = False + + def get_queryset(self): + queryset = HorillaListView.get_queryset(self) + queryset = queryset.filter( + approved=False, canceled=False, employee_id__is_active=True + ) + queryset = filtersubordinates(self.request, queryset, "base.add_shiftrequest") + return queryset + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter("employee.view_employeeworkinformation"), name="dispatch" +) +class EmployeeWorkInformationList(HorillaListView): + """ + Employee work information progress list + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "pending" + self.search_url = reverse("emp-workinfo-complete") + + model = EmployeeWorkInformation + filter_class = EmployeeWorkInformationFilter + bulk_select_option = False + show_toggle_form = False + + columns = [ + (_("Employee"), "employee_id"), + (_("Progress"), "progress_col"), + ] + + header_attrs = { + "employee_id": """ + style ="width:100px !important" + """ + } + + row_attrs = """ + hx-get='{get_edit_url}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + def get_queryset(self): + queryset = super().get_queryset() + queryset = filtersubordinates( + self.request, queryset, "employee.view_employeeworkinformation" + ) + queryset = queryset.filter( + id__in=[obj.id for obj in queryset if obj.calculate_progress() != 100] + ) + return queryset + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter("employee.change_employeeworkinformation"), name="dispatch" +) +class EmployeeWorkInformationFormView(HorillaFormView): + """ + form view for edit work information + """ + + form_class = EmployeeWorkInformationUpdateForm + model = EmployeeWorkInformation + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Work Information") + return context + + def form_valid(self, form: EmployeeWorkInformationUpdateForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Work Information Updated") + messages.success(self.request, message) + form.save() + return HttpResponse( + "" + ) + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class DashboardAnnouncementView(HorillaListView): + """ + list view for dashboard announcement + """ + + model = Announcement + filter_class = AnnouncementFilter + show_toggle_form = False + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("dashboard-announcement-list") + + columns = [ + (_("Title"), "announcement_custom_col"), + ] + + bulk_select_option = False + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.exclude(expire_date__lt=datetime.today().date()).order_by( + "-created_at" + ) + + return queryset + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.view_announcement"), name="dispatch") +class AnnouncementViewedByList(HorillaListView): + """ + List view for announcement viewed by on detail view + """ + + model = AnnouncementView + filter_class = AnnouncementViewFilter + bulk_select_option = False + show_toggle_form = False + + columns = [ + (_("Viewed By"), "announcement_viewed_by_col"), + ] + + def get_queryset(self): + queryset = super().get_queryset() + anoun = self.kwargs.get("announcement_id") + queryset = queryset.filter(announcement_id__id=anoun, viewed=True) + return queryset diff --git a/base/cbv/department.py b/base/cbv/department.py new file mode 100644 index 000000000..61c9088b9 --- /dev/null +++ b/base/cbv/department.py @@ -0,0 +1,165 @@ +""" +this page is handling the cbv methods for department in settings +""" + +from typing import Any + +from django.contrib import messages +from django.http import HttpResponse +from django.middleware.csrf import get_token +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ + +from base.filters import DepartmentViewFilter +from base.forms import DepartmentForm +from base.models import Department +from horilla.horilla_middlewares import _thread_locals +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_department"), name="dispatch") +class DepartmentListView(HorillaListView): + """ + list view for department in settings + """ + + model = Department + filter_class = DepartmentViewFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("department-list") + self.actions = [] + if self.request.user.has_perm("base.change_department"): + self.actions.append( + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + + if self.request.user.has_perm("base.delete_department"): + self.actions.append( + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.Department&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + # + + row_attrs = """ + id="departmentTr{get_delete_instance}" + """ + + columns = [ + (_("Department"), "department"), + ] + sortby_mapping = [ + (_("Department"), "department"), + ] + + header_attrs = { + "department": """ style="width:300px !important" """, + "action": """ style="width:180px !important" """, + } + + records_per_page = 7 + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_department"), name="dispatch") +class DepartmentNavView(HorillaNavView): + """ + nav bar of the department view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("department-list") + if self.request.user.has_perm("base.add_department"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('settings-department-creation')}" + """ + + nav_title = _("Department") + search_swap_target = "#listContainer" + filter_instance = DepartmentViewFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_department"), name="dispatch") +class DepartmentCreateForm(HorillaFormView): + """ + form view for creating and editing departments in settings + """ + + model = Department + form_class = DepartmentForm + new_display_title = _("Create Department") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = self.form_class() + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Department") + context[form] = form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Department") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: DepartmentForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + messages.success(self.request, _("Department updated")) + else: + messages.success( + self.request, _("Department has been created successfully!") + ) + form.save() + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/employee_shift.py b/base/cbv/employee_shift.py new file mode 100644 index 000000000..fb2e2bfb3 --- /dev/null +++ b/base/cbv/employee_shift.py @@ -0,0 +1,106 @@ +""" +This page handles employee shift page in settings +""" + +from typing import Any +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import EmployeeShiftFilter +from base.models import EmployeeShift +from horilla_views.generic.cbv.views import ( + HorillaListView, + HorillaNavView, +) +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_employeeshift"), name="dispatch") +class EmployeeShiftListView(HorillaListView): + """ + List view of the employee shift page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # self.action_method = "actions_col" + self.view_id = "shift_view" + self.search_url = reverse("employee-shift-list") + self.actions = [] + if self.request.user.has_perm("base.change_employeeshift"): + self.actions.append( + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get="{get_update_url}?instance_ids={ordered_ids}" + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + if self.request.user.has_perm("base.delete_employeeshift"): + self.actions.append( + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.employeeshift&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = EmployeeShift + filter_class = EmployeeShiftFilter + + bulk_update_fields = [ + "weekly_full_time", + "full_time", + ] + + columns = [ + (_("Shift"), "employee_shift"), + (_("Weekly Full Time"), "weekly_full_time"), + (_("Full Time"), "full_time"), + ] + + sortby_mapping = [ + ("Shift", "employee_shift"), + ("Weekly Full Time", "weekly_full_time"), + ("Full Time", "full_time"), + ] + + row_attrs = """ + id = "shiftTr{get_instance_id}" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_employeeshift"), name="dispatch") +class EmployeeShiftNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("employee-shift-list") + if self.request.user.has_perm("base.add_employeeshift"): + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('employee-shift-create-view')}" + """ + + nav_title = _("Shift") + filter_instance = EmployeeShiftFilter() + search_swap_target = "#listContainer" diff --git a/base/cbv/employee_shift_shedule.py b/base/cbv/employee_shift_shedule.py new file mode 100644 index 000000000..8ece1f12d --- /dev/null +++ b/base/cbv/employee_shift_shedule.py @@ -0,0 +1,151 @@ +""" +this page is handling the cbv methods for Employee shift shedule in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import EmployeeShiftFilter, EmployeeShiftScheduleFilter +from base.forms import EmployeeShiftScheduleForm +from base.models import EmployeeShift, EmployeeShiftSchedule +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_employeeshiftschedule"), name="dispatch" +) +class EmployeeShiftSheduleNav(HorillaNavView): + """ + nav bar of the employee shift sheduel view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("employee-shift-shedule-list") + if self.request.user.has_perm("base.add_employeeshiftschedule"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('settings-employee-shift-shedule-create')}" + """ + + nav_title = _("Shift Schedule") + search_swap_target = "#listContainer" + filter_instance = EmployeeShiftFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_employeeshiftschedule"), name="dispatch") +class EmployeeShiftSheduleCreateForm(HorillaFormView): + """ + form view for creating and updating job position in settings + """ + + model = EmployeeShiftSchedule + form_class = EmployeeShiftScheduleForm + new_display_title = _("Create Employee Shift Schedule") + template_name = "cbv/settings/employee_shift_schedule_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form.fields["day"].initial = self.form.instance.day + self.form_class.verbose_name = _("Update Employee Shift Schedule") + return context + + def form_valid(self, form: EmployeeShiftScheduleForm) -> HttpResponse: + if form.is_valid(): + if self.form.instance.pk: + shifts = form.instance + if shifts is not None: + days = form["day"].value() + if days: + shifts.day_id = days + shifts.save() + messages.success(self.request, _("Shift schedule Updated!.")) + else: + form.save() + messages.success( + self.request, + _("Employee Shift Schedule has been created successfully!"), + ) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_employeeshiftschedule"), name="dispatch" +) +class EmployeeShiftSheduleList(HorillaListView): + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "day-container" + self.search_url = reverse("employee-shift-shedule-list") + if not self.request.user.has_perm( + "base.change_employeeshiftschedule" + ) and not self.request.user.has_perm("base.delete_employeeshiftschedule"): + self.action_method = None + + bulk_update_fields = ["start_time", "end_time", "minimum_working_hour"] + + model = EmployeeShiftSchedule + filter_class = EmployeeShiftScheduleFilter + show_filter_tags = False + + columns = [ + (_("Day"), "day_col", "get_avatar"), + (_("Start Time"), "start_time"), + (_("End Time"), "end_time"), + (_("Minimum Working Hours"), "minimum_working_hour"), + (_("Auto Check Out"), "auto_punch_out_col"), + ] + + header_attrs = { + "action": """ + style="width:200px !important;" + """ + } + + row_attrs = """ + id = "scheduleTr{get_instance_id}" + hx-get='{get_detail_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + action_method = "actions_col" + + +class EmployeeShiftSheduleDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + model = EmployeeShiftSchedule + title = _("Details") + header = {"title": "day", "subtitle": "shift_id", "avatar": "get_avatar"} + body = [ + (_("Start Time"), "start_time"), + (_("End Time"), "end_time"), + (_("Minimum Working Hours"), "minimum_working_hour"), + (_("Auto Check Out"), "auto_punch_out_col"), + (_("Automatic Check Out Time"), "get_automatic_check_out_time", True), + ] + + action_method = "detail_actions_col" diff --git a/base/cbv/employee_type.py b/base/cbv/employee_type.py new file mode 100644 index 000000000..6ee5ebb64 --- /dev/null +++ b/base/cbv/employee_type.py @@ -0,0 +1,131 @@ +""" +This page handles employee type in settings page +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import EmployeeTypeFilter +from base.forms import EmployeeTypeForm +from base.models import EmployeeType +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_employeetype"), name="dispatch") +class EmployeeTypeListView(HorillaListView): + """ + List view of the resticted days page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "employee_type" + self.search_url = reverse("employee-type-list") + self.actions = [] + if self.request.user.has_perm("base.change_employeetype"): + self.actions.append( + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get="{get_update_url}?instance_ids={ordered_ids}" + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + if self.request.user.has_perm("base.delete_employeetype"): + self.actions.append( + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.employeetype&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = EmployeeType + filter_class = EmployeeTypeFilter + + columns = [ + (_("Employee Type"), "employee_type"), + ] + header_attrs = { + "employee_type": """ + style="width:400px !important;" + """ + } + + row_attrs = """ + id = "employeeTypeTr{get_instance_id}" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_employeetype"), name="dispatch") +class EmployeeTypeNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("employee-type-list") + if self.request.user.has_perm("base.add_employeetype"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('employee-type-create-view')}" + """ + + nav_title = _("Employee Type") + filter_instance = EmployeeTypeFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_employeetype"), name="dispatch") +class EmployeeTypeFormView(HorillaFormView): + """ + Create and edit form + """ + + model = EmployeeType + form_class = EmployeeTypeForm + new_display_title = _("Create Employee Type") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Employee Type") + return context + + def form_valid(self, form: EmployeeTypeForm) -> HttpResponse: + + if form.is_valid(): + if form.instance.pk: + message = _("The employee type updated successfully.") + else: + message = _("The employee type created successfully.") + form.save() + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/holidays.py b/base/cbv/holidays.py new file mode 100644 index 000000000..6424dc030 --- /dev/null +++ b/base/cbv/holidays.py @@ -0,0 +1,212 @@ +""" +this page is handling the cbv methods of holiday page +""" + +from typing import Any +from django.contrib import messages +from django.http import HttpResponse +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + TemplateView, + HorillaListView, + HorillaNavView, + HorillaFormView, +) +from horilla_views.cbv_methods import login_required, permission_required +from base.filters import HolidayFilter +from base.forms import HolidayForm, HolidaysColumnExportForm +from base.models import Holidays + + +@method_decorator(login_required, name="dispatch") +class HolidaysView(TemplateView): + """ + for page view + """ + + template_name = "cbv/holidays/holidays_home.html" + + + +@method_decorator(login_required, name="dispatch") +class HolidayListView(HorillaListView): + """ + list view + """ + + bulk_update_fields = [ + "recurring" + ] + + filter_class = HolidayFilter + model = Holidays + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("holiday-filter") + self.view_id = "holidaydelete" + if self.request.user.has_perm("add_holiday"): + self.action_method = "holidays_actions" + + columns = [ + (_("Holiday Name"), "name"), + (_("Start Date"), "start_date"), + (_("End Date"), "end_date"), + (_("Recurring"), "get_recurring_status"), + ] + + header_attrs = { + "name": """ style="width:200px !important;" + """, + } + + + sortby_mapping = [ + ("Holiday Name", "name"), + ("Start Date", "start_date"), + ("End Date", "end_date"), + ] + + row_attrs = """ + hx-get='{detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + records_per_page = 10 + + +@method_decorator(login_required, name="dispatch") +class HolidayNavView(HorillaNavView): + """ + nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("holiday-filter") + if self.request.user.has_perm("add_holiday"): + self.create_attrs = f""" + hx-get="{reverse_lazy('holiday-creation')}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + self.actions = [ + { + "action": _("Import"), + "attrs": """ + onclick=" + importHolidays(); + " + data-toggle = "oh-modal-toggle" + data-target = "#holidayImport + " + style="cursor: pointer;" + """, + }, + { + "action": _("Export"), + "attrs": f""" + data-toggle = "oh-modal-toggle" + data-target = "#genericModal" + hx-target="#genericModalBody" + hx-get ="{reverse('holiday-nav-export')}" + style="cursor: pointer;" + """, + }, + { + "action": _("Delete"), + "attrs": """ + onclick=" + bulkDeleteHoliday(); + " + data-action ="delete" + style="cursor: pointer; color:red !important" + """, + }, + ] + + nav_title = _("Holidays") + filter_body_template = "cbv/holidays/holiday_filter.html" + filter_form_context_name = "form" + filter_instance = HolidayFilter() + search_swap_target = "#listContainer" + +@method_decorator(login_required, name="dispatch") +class HolidayDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + model = Holidays + title = _("Details") + + header = { + "title": "name", + "subtitle":"", + "avatar":"get_avatar" + } + body = { + (_("Holiday Name"), "name"), + (_("Start Date"), "start_date"), + (_("End Date"), "end_date"), + (_("Recurring"), "get_recurring_status"), + } + + action_method = "detail_view_actions" + +@method_decorator(login_required, name="dispatch") +class HolidayExport(TemplateView): + """ + for bulk export + """ + + template_name = "cbv/holidays/holidays_export.html" + + def get_context_data(self, **kwargs: Any): + """ + get data for export + """ + + holiday = Holidays.objects.all() + export_column = HolidaysColumnExportForm + export_filter = HolidayFilter(queryset=holiday) + context = super().get_context_data(**kwargs) + context["export_column"] = export_column + context["export_filter"] = export_filter + return context + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("leave.add_holiday"),name="dispatch") +class HolidayFormView(HorillaFormView): + """ + form view for create button + """ + + form_class = HolidayForm + model = Holidays + new_display_title = _("Create Holiday") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Holiday") + + return context + + def form_valid(self, form: HolidayForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Holiday Updated Successfully") + else: + message = _("New Holiday Created Successfully") + form.save() + + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/job_position.py b/base/cbv/job_position.py new file mode 100644 index 000000000..b85f58a8b --- /dev/null +++ b/base/cbv/job_position.py @@ -0,0 +1,123 @@ +""" +this page is handling the cbv methods for Job Position in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import DepartmentViewFilter +from base.forms import JobPositionForm +from base.models import Department, JobPosition +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_jobposition"), name="dispatch") +class JobPositionListView(HorillaListView): + """ + list view for job positions in settings + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("job-position-list") + self.view_id = "job_position" + + + model = Department + filter_class = DepartmentViewFilter + + columns = [ + (_("Department"), "get_department_col"), + (_("Job Position"), "get_job_position_col"), + ] + + row_attrs = """ + class="oh-sticky-table__tr oh-permission-table__tr oh-permission-table--collapsed" + data-label="Job Position" + data-count="{toggle_count}" + """ + + header_attrs = { + "get_department_col": """ style="width:300px !important; " """, + "get_job_position_col": """ style="width:300px !important; " """ + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_jobposition"), name="dispatch") +class JobPositionNavView(HorillaNavView): + """ + nav bar of the job position view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("job-position-list") + if self.request.user.has_perm("base.add_jobposition"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('job-position-create-form')}" + """ + + nav_title = _("Job Position") + search_swap_target = "#listContainer" + filter_instance = DepartmentViewFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_jobposition"), name="dispatch") +class JobPositionCreateForm(HorillaFormView): + """ + form view for creating job position in settings + """ + + model = JobPosition + form_class = JobPositionForm + new_display_title = _("Create Job Position") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form.fields["department_id"].initial = self.form.instance.department_id + self.form_class.verbose_name = _("Update Job Position") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Job Position") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: JobPositionForm) -> HttpResponse: + job_position = form.instance + if self.form.instance.pk and form.is_valid(): + if job_position is not None: + department_id = form.cleaned_data.get("department_id") + if department_id: + job_position.department_id = department_id + job_position.save() + messages.success(self.request, _("Job position updated.")) + elif ( + not form.instance.pk + and form.data.getlist("department_id") + and form.data.get("job_position") + ): + form.save(commit=True) + return self.HttpResponse() diff --git a/base/cbv/job_role.py b/base/cbv/job_role.py new file mode 100644 index 000000000..5f69814d0 --- /dev/null +++ b/base/cbv/job_role.py @@ -0,0 +1,122 @@ +""" +this page is handling the cbv methods for Job role in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.urls import reverse +from django.contrib import messages +from base.filters import JobRoleFilter +from base.forms import JobRoleForm +from base.models import JobPosition, JobRole +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_jobrole"), name="dispatch") +class JobRoleListView(HorillaListView): + """ + List view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "job_role" + self.search_url = reverse("job-role-list") + + model = JobPosition + filter_class = JobRoleFilter + + columns = [ + (_("Job Position"), "job_position_col"), + (_("Job Role"), "job_role_col"), + ] + header_attrs = { + "job_position_col": """ + style="width:300px !important;" + """, + "job_role_col": """ + style="width:300px !important;" + """, + } + + row_attrs = """ + class = "oh-sticky-table__tr oh-permission-table__tr oh-permission-table--collapsed" + data-count="{get_data_count}" + data-label="Job Role" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_jobrole"), name="dispatch") +class JobRoleNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("job-role-list") + if self.request.user.has_perm("base.add_jobrole"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('create-job-role')}" + """ + + nav_title = _("Job Role") + filter_instance = JobRoleFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_jobrole"), name="dispatch") +class JobRoleFormView(HorillaFormView): + """ + Create and edit form + """ + + model = JobRole + form_class = JobRoleForm + new_display_title = _("Create Job Role") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Job Role") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: JobRoleForm) -> HttpResponse: + if self.form.instance.pk and form.is_valid(): + instance = form.instance + job_position = form.cleaned_data.get("job_position_id") + instance.job_position_id = job_position + instance.save() + messages.success( + self.request, _("Job role has been updated successfully!") + ) + elif ( + not self.form.instance.pk + and self.form.data.getlist("job_position_id") + and self.form.data.get("job_role") + ): + form.save(commit=True) + return self.HttpResponse() diff --git a/base/cbv/mail_log_tab.py b/base/cbv/mail_log_tab.py new file mode 100644 index 000000000..674986678 --- /dev/null +++ b/base/cbv/mail_log_tab.py @@ -0,0 +1,110 @@ +""" +This page is handling the cbv methods of mail log tab in employee individual page. +""" + +from typing import Any +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from django.utils.decorators import method_decorator +from base.filters import MailLogFilter +from base.models import EmailLog +from employee.models import Employee +from horilla_views.generic.cbv.views import HorillaDetailedView, HorillaListView +from horilla_views.cbv_methods import login_required +from accessibility.cbv_decorators import enter_if_accessible +from accessibility.cbv_decorators import enter_if_accessible + + +def _check_reporting_manager(request, *args, **kwargs): + return request.user.employee_get.reporting_manager.exists() + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + enter_if_accessible( + feature="view_mail_log", + perm="employee.view_employee", + method=_check_reporting_manager, + ), + name="dispatch", +) +class MailLogTabList(HorillaListView): + """ + list view for mail log tab + """ + + model = EmailLog + records_per_page = 5 + filter_class = MailLogFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "maillog" + + pk = self.request.resolver_match.kwargs.get("pk") + self.search_url = reverse("individual-email-log-list", kwargs={"pk": pk}) + + # def get_context_data(self, **kwargs: Any): + # context = super().get_context_data(**kwargs) + # pk = self.kwargs.get('pk') + # context["search_url"] = f"{reverse('individual-email-log-list',kwargs={'pk': pk})}" + # return context + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + employee = Employee.objects.get(id=pk) + query_filter = Q(to__icontains=employee.email) + queryset = queryset.filter(to__icontains=employee.email) + if employee.employee_work_info and employee.employee_work_info.email: + query_filter |= Q(to__icontains=employee.employee_work_info.email) + queryset = queryset.filter(query_filter) + queryset = queryset.order_by("-created_at") + + return queryset + + columns = [ + (_("Subject"), "subject"), + (_("Date"), "created_at"), + (_("Status"), "status_display"), + ] + + sortby_mapping = [ + (_("Subject"), "subject"), + (_("Date"), "created_at"), + ] + + row_attrs = """ + hx-get='{mail_log_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + enter_if_accessible( + feature="view_mail_log", + perm="employee.view_employee", + method=_check_reporting_manager, + ), + name="dispatch", +) +class MailLogDetailView(HorillaDetailedView): + """ + detail view for mail log tab + """ + + template_name = "cbv/mail_log_tab/iframe.html" + model = EmailLog + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + log = EmailLog.objects.filter(id=pk).first() + context["log"] = log + return context + + header = {"title": "", "subtitle": "", "avatar": ""} diff --git a/base/cbv/mail_server.py b/base/cbv/mail_server.py new file mode 100644 index 000000000..0fb0aa3f8 --- /dev/null +++ b/base/cbv/mail_server.py @@ -0,0 +1,121 @@ +""" +This page handles the mail server page in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.urls import reverse +from django.contrib import messages +from base.filters import MailServerFilter +from base.forms import DynamicMailConfForm +from base.models import DynamicEmailConfiguration +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_dynamicemailconfiguration"), name="dispatch" +) +class MailServerListView(HorillaListView): + """ + List view of the resticted days page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # self.action_method = "actions_col" + self.view_id = "mail-server-cont" + self.search_url = reverse("mail-server-list") + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + primary_mail_not_exist = True + if DynamicEmailConfiguration.objects.filter(is_primary=True).exists(): + primary_mail_not_exist = False + context["primary_mail_not_exist"] = primary_mail_not_exist + return context + + model = DynamicEmailConfiguration + filter_class = MailServerFilter + template_name = "cbv/settings/extended_mail_server.html" + + columns = [ + (_("Host User"), "username"), + (_("Host"), "host"), + (_("Compnay"), "company_id"), + ] + + header_attrs = { + "action" : """ + style="width:200px !important" + """ + } + + row_attrs = "{highlight_cell}" + action_method = "action_col" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_dynamicemailconfiguration"), name="dispatch" +) +class MailServerNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("mail-server-list") + if self.request.user.has_perm("base.add_dynamicemailconfiguration"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('create-mail-server')}" + """ + + nav_title = _("Mail Servers") + filter_instance = MailServerFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.add_dynamicemailconfiguration"), name="dispatch" +) +class MailServerFormView(HorillaFormView): + """ + Create and edit form + """ + + model = DynamicEmailConfiguration + form_class = DynamicMailConfForm + new_display_title = _("Create Mail Server") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Mail Server") + return context + + def form_valid(self, form: DynamicMailConfForm) -> HttpResponse: + + if form.is_valid(): + if form.instance.pk: + message = _("Mail server updated successfully.") + else: + message = _("Mail server created successfully.") + form.save() + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/mail_template.py b/base/cbv/mail_template.py new file mode 100644 index 000000000..73e9c9993 --- /dev/null +++ b/base/cbv/mail_template.py @@ -0,0 +1,101 @@ +""" +This page handles the cbv methods for mail template page +""" + +from typing import Any +from django import forms +from django.http import HttpResponse +from django.contrib import messages +from django.shortcuts import render +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from horilla_views.generic.cbv.views import HorillaFormView +from horilla_views.cbv_methods import login_required, permission_required +from base.models import HorillaMailTemplate +from base.forms import MailTemplateForm + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.add_horillamailtemplate"),name="dispatch") +class MailTemplateFormView(HorillaFormView): + """ + form view for create and edit mail template + """ + + form_class = MailTemplateForm + model = HorillaMailTemplate + template_name = "cbv/mail_template/form_inherit.html" + new_display_title = _("Add Template") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Template") + + return context + + def form_valid(self, form: MailTemplateForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Template Updated") + else: + message = _("Template created") + form.save() + + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.add_horillamailtemplate"),name="dispatch") +class MailTemplateDuplicateForm(HorillaFormView): + """ + from view for duplicate mail templates + """ + + model = HorillaMailTemplate + form_class = MailTemplateForm + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = HorillaMailTemplate.objects.get(id=self.kwargs["pk"]) + form = self.form_class(instance=original_object) + + for field_name, field in form.fields.items(): + if isinstance(field, forms.CharField): + initial_value = form.initial.get(field_name, "") + if initial_value: + initial_value += " (copy)" + form.initial[field_name] = initial_value + form.fields[field_name].initial = initial_value + + if hasattr(form.instance, "id"): + form.instance.id = None + + context["form"] = form + self.form_class.verbose_name = _("Duplicate Template") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + self.form_class.verbose_name = _("Duplicate Template") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: MailTemplateForm) -> HttpResponse: + form = self.form_class(self.request.POST) + self.form_class.verbose_name = _("Duplicate Template") + if form.is_valid(): + message = _("Template Added") + messages.success(self.request, message) + form.save() + return self.HttpResponse("") + return self.form_invalid(form) + + + diff --git a/base/cbv/multiple_approval_condition.py b/base/cbv/multiple_approval_condition.py new file mode 100644 index 000000000..097cbd842 --- /dev/null +++ b/base/cbv/multiple_approval_condition.py @@ -0,0 +1,260 @@ +""" +Multiple approval condition page +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from django import forms +from base.filters import MultipleApprovalConditionFilter +from base.forms import MultipleApproveConditionForm +from base.models import MultipleApprovalCondition, MultipleApprovalManagers +from base.widgets import CustomModelChoiceWidget +from employee.models import Employee +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_multipleapprovalcondition"), name="dispatch" +) +class MultipleApprovalConditionView(TemplateView): + """ + for Multiple approval condition page + """ + + template_name = "cbv/multiple_approval_condition/multiple_approval_condition.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_multipleapprovalcondition"), name="dispatch" +) +class MultipleApprovalConditionList(HorillaListView): + """ + List view of the resticted days page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.search_url = reverse("hx-multiple-approval-condition") + self.action_method = "actions_col" + self.view_id = "multipleApproveCondition" + + model = MultipleApprovalCondition + filter_class = MultipleApprovalConditionFilter + + columns = [ + (_("Department"), "department"), + (_("Condition Field"), "get_condition_field"), + (_("Condition Operator"), "get_condition_operator"), + (_("Condition Value"), "get_condition_value"), + (_("Approval Managers"), "approval_managers_col"), + (_("Company"),"company_id") + ] + + header_attrs = { + "department": """ style="width:180px !important" """, + "approval_managers_col": """ style="width:200px !important" """, + + } + + sortby_mapping = [ + ("Department", "department__department"), + ("Condition Operator", "get_condition_operator"), + ("Condition Value", "get_condition_value"), + ] + + row_attrs = """ + hx-get='{detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_multipleapprovalcondition"), name="dispatch" +) +class MultipleApprovalConditionNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("hx-multiple-approval-condition") + if self.request.user.has_perm("base.add_multipleapprovalcondition"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('multiple-level-approval-create')}" + """ + + nav_title = _("Multiple Approval Condition") + filter_instance = MultipleApprovalConditionFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="base.view_multipleapprovalcondition"), name="dispatch" +) +class MultipleApprovalConditionDetailView(HorillaDetailedView): + """ + detail view of page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + (_("Condition Field"), "get_condition_field"), + (_("Condition Operator"), "get_condition_operator"), + (_("Condition Value"), "get_condition_value"), + (_("Approval Managers"), "approval_managers_col"), + ] + + action_method = "detail_actions" + + model = MultipleApprovalCondition + title = _("Details") + header = { + "title": "department", + "subtitle": "", + "avatar": "get_avatar", + } + + +class MultipleApprovalConditionFormView(HorillaFormView): + """ + Create and edit form + """ + + model = MultipleApprovalCondition + form_class = MultipleApproveConditionForm + new_display_title = _("Create Multiple Approval Condition") + template_name = "cbv/multiple_approval_condition/form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form.fields["condition_operator"].widget.attrs[ + "hx-target" + ] = "#id_condition_value_parent_div" + self.form.fields["condition_operator"].widget.attrs["hx-swap"] = "innerHTML" + return context + + def form_valid(self, form: MultipleApproveConditionForm) -> HttpResponse: + if form.is_valid(): + instance = form.save() + sequence = 0 + if form.instance.pk: + MultipleApprovalManagers.objects.filter( + condition_id=self.form.instance + ).delete() + message = _("Multiple approval conditon Created Successfully") + condition_approval_managers = self.request.POST.getlist( + "multi_approval_manager" + ) + 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, + ) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +class EditApprovalConditionFormView(MultipleApprovalConditionFormView): + """ + Edit form + """ + + template_name = "cbv/multiple_approval_condition/form_edit.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.form.fields["condition_operator"].widget.attrs["hx-swap"] = "innerHTML" + self.form.fields["condition_operator"].widget.attrs[ + "hx-target" + ] = "#id_condition_value_parent_div" + managers = MultipleApprovalManagers.objects.filter( + condition_id=self.form.instance + ).order_by("sequence") + self.approval_managers_edit(self.form, managers) + context["managers_count"] = len(managers) + context["form"] = self.form + self.form_class.verbose_name = _("Update Multiple Approval Condition") + return context + + def approval_managers_edit(self, form, managers): + for i, manager in enumerate(managers): + if i == 0: + form.initial["multi_approval_manager"] = managers[0].employee_id + else: + field_name = f"multi_approval_manager_{i}" + form.fields[field_name] = forms.ModelChoiceField( + queryset=Employee.objects.all(), + label=_("Approval Manager") if i == 0 else "", + widget=CustomModelChoiceWidget( + delete_url="/configuration/remove-approval-manager", + attrs={ + "class": "oh-select oh-select-2 mb-3", + "name": field_name, + "id": f"id_{field_name}", + }, + ), + required=False, + ) + form.initial[field_name] = manager.employee_id + + def form_valid(self, form: MultipleApproveConditionForm) -> HttpResponse: + if form.is_valid(): + instance = form.save() + sequence = 0 + if self.form.instance.pk: + MultipleApprovalManagers.objects.filter( + condition_id=self.form.instance + ).delete() + message = _("Multiple approval conditon updated Successfully") + for key, value in self.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, + ) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) diff --git a/base/cbv/penalty.py b/base/cbv/penalty.py new file mode 100644 index 000000000..a2bbca489 --- /dev/null +++ b/base/cbv/penalty.py @@ -0,0 +1,40 @@ +from typing import Any +from django.urls import reverse +from base.filters import PenaltyFilter +from base.models import PenaltyAccounts +from horilla_views.generic.cbv.views import HorillaListView +from django.utils.translation import gettext_lazy as _ + + +class ViewPenaltyList(HorillaListView): + """ + List view of penalty + """ + + bulk_select_option = False + + + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("view-penalties") + self.view_id = "view-penalty" + + model = PenaltyAccounts + filter_class = PenaltyFilter + columns = [ + (_("Penalty amount"), "penalty_amount"), + (_("Created Date"), "created_at"), + ] + + header_attrs = { + "penalty_amount": """ + style="width:180px !important;" + """, + "created_at": """ + style="width:180px !important;" + """, + + } + + diff --git a/base/cbv/rotating_shift.py b/base/cbv/rotating_shift.py new file mode 100644 index 000000000..e5f1fde0d --- /dev/null +++ b/base/cbv/rotating_shift.py @@ -0,0 +1,107 @@ +""" +This page handles rotating shift types page in settings. +""" + +from typing import Any +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from base.filters import RotatingShiftFilter +from base.models import RotatingShift +from horilla_views.generic.cbv.views import ( + HorillaListView, + HorillaNavView, +) +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_rotatingshift"), name="dispatch") +class RotatingShiftTypeListView(HorillaListView): + """ + List view of the employee shift page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # self.action_method = "actions_col" + self.view_id = "shift_view" + self.search_url = reverse("rotating-shift-list") + self.actions = [] + if self.request.user.has_perm("base.change_rotatingshift"): + self.actions.append( + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get="{get_update_url}?instance_ids={ordered_ids}" + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + if self.request.user.has_perm("base.delete_rotatingshift"): + self.actions.append( + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.rotatingshift&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = RotatingShift + filter_class = RotatingShiftFilter + + columns = [ + (_("Title"), "name"), + (_("Shift 1"), "shift1"), + (_("Shift 2"), "shift2"), + (_("Additional Shifts"), "get_additional_shifts"), + ] + + sortby_mapping = [ + ("Title", "name"), + ("Shift 1", "shift1__employee_shift"), + ("Shift 2", "shift2__employee_shift"), + ("Additional Shifts", "get_additional_shifts"), + ] + + row_attrs = """ + id = "rotatingShiftTr{get_instance_id}" + """ + + header_attrs = { + "name": """ style="width:200px !important" """, + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.view_rotatingshift"), name="dispatch") +class RotatingShiftTypeNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-shift-list") + if self.request.user.has_perm("base.add_rotatingshift"): + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('rotating-shift-create')}" + """ + + nav_title = _("Rotating Shift") + filter_instance = RotatingShiftFilter() + search_swap_target = "#listContainer" diff --git a/base/cbv/rotating_shift_assign.py b/base/cbv/rotating_shift_assign.py new file mode 100644 index 000000000..4fdc83b74 --- /dev/null +++ b/base/cbv/rotating_shift_assign.py @@ -0,0 +1,424 @@ +""" +Rotating shift request + +""" + +import contextlib +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from base.decorators import manager_can_enter +from base.filters import RotatingShiftAssignFilters +from base.forms import ( + RotatingShiftAssignExportForm, + RotatingShiftAssignForm, + RotatingShiftForm, +) +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from base.models import RotatingShift, RotatingShiftAssign +from employee.models import Employee +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) +from notifications.signals import notify + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingShiftAssignView(TemplateView): + """ + Shift request page + """ + + template_name = "cbv/rotating_shift/rotating_shift.html" + + +@method_decorator(login_required, name="dispatch") +class RotatingShiftListParent(HorillaListView): + """ + Parent class + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-shift-request-list") + # self.filter_keys_to_remove = ["deleted"] + self.view_id = "rotating-shift-container" + if ( + self.request.user.has_perm("base.delete_rotatingshiftassign") + or self.request.user.has_perm("base.change_rotatingshiftassign") + or is_reportingmanager(self.request) + ): + self.action_method = "actions" + else: + self.action_method = None + + model = RotatingShiftAssign + filter_class = RotatingShiftAssignFilters + template_name = "cbv/rotating_shift/extended_rotating_shift.html" + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Rotating Shift"), "rotating_shift_id"), + (_("Based on"), "get_based_on_display"), + (_("Rotate"), "rotating_column"), + (_("Start Date"), "start_date"), + (_("Current Shift"), "current_shift"), + (_("Next Switch"), "next_change_date"), + (_("Next Shift"), "next_shift"), + ] + + header_attrs = { + "action": """ + style="width:250px !important;" + """ + } + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Rotating Shift", "rotating_shift_id__name"), + ("Based on", "get_based_on_display"), + ("Rotate", "rotating_column"), + ("Next Shift", "next_shift__employee_shift"), + ("Start Date", "start_date"), + ("Current Shift", "current_shift__employee_shift"), + ("Next Switch", "next_change_date"), + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingShiftList(RotatingShiftListParent): + """ + List view + """ + + bulk_update_fields = ["rotating_shift_id", "start_date"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = filtersubordinates( + self.request, queryset, "base.view_rotatingshiftassign" + ) + return queryset + + row_attrs = """ + hx-get='{rotating_shift_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingShiftAssignNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-shift-request-list") + if self.request.user.has_perm("base.add_rotatingshiftassign") or is_reportingmanager(self.request): + self.create_attrs = f""" + hx-get="{reverse_lazy('rotating-shift-assign-add')}" + data-toggle="oh-modal-toggle" + data-target="#genericMaodal" + hx-target="#genericModalBody" + """ + self.actions = [] + + if self.request.user.has_perm("base.view_rotatingshiftassign") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Import"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#shiftImport" + class="oh-dropdown__link" + role = "button" + onclick="template_download(event)" + """, + }, + ) + + if self.request.user.has_perm("base.view_rotatingshiftassign") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Export"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('export-rshift')}" + hx-target ="#genericModalBody" + style="cursor: pointer;" + """, + }, + ) + if self.request.user.has_perm("base.change_rotatingshiftassign") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Archive"), + "attrs": """ + onclick = "archiveRotateShift();" + style="cursor: pointer;" + """, + } + ) + self.actions.append( + { + "action": _("Un-Archive"), + "attrs": """ + onclick = "un_archiveRotateShift();" + style="cursor: pointer;" + """, + } + ) + if self.request.user.has_perm("base.delete_rotatingshiftassign") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Delete"), + "attrs": """ + class ="shift" + onclick = "deleteRotatingShift();" + data-action ="delete" + style="cursor: pointer; color:red !important" + """, + } + ) + + + nav_title = "Rotating Shift Assign" + filter_body_template = "cbv/rotating_shift/rotating_shift_filter.html" + filter_instance = RotatingShiftAssignFilters() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("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_position_id", _("Job role")), + ( + "employee_id__employee_work_info__reporting_manager_id", + _("Reporting Manager"), + ), + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingShiftDetailview(HorillaDetailedView): + """ + Detail View + """ + + model = RotatingShiftAssign + + title = _("Details") + + header = { + "title": "employee_id__get_full_name", + "subtitle": "rotating_subtitle", + "avatar": "employee_id__get_avatar", + } + + body = [ + (_("Title"), "rotating_shift_id"), + (_("Based on"), "get_based_on_display"), + (_("Rotate"), "rotating_column"), + (_("Start Date"), "start_date"), + (_("Current Shift"), "current_shift"), + (_("Next Shift"), "next_shift"), + (_("Next Change Date"), "next_change_date"), + (_("Status"), "check_active"), + ] + + action_method = "rotating_detail_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingExportView(TemplateView): + """ + For candidate export + """ + + template_name = "cbv/rotating_shift/rshift_export.html" + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + rshift_requests = RotatingShiftAssign.objects.all() + export_columns = RotatingShiftAssignExportForm + export_filter = RotatingShiftAssignFilters(queryset=rshift_requests) + context["export_columns"] = export_columns + context["export_filter"] = export_filter + return context + + +class DynamicRotatingShiftTypeFormView(HorillaFormView): + """ + form view + """ + + model = RotatingShift + form_class = RotatingShiftForm + new_display_title = "Create Rotating Shift" + is_dynamic_create_view = True + template_name = "cbv/rotating_shift/rot_shift_form.html" + + def form_valid(self, form: RotatingShiftForm) -> HttpResponse: + if form.is_valid(): + form.save() + message = _("Rotating Shift Created") + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.add_rotatingshift"), name="dispatch") +class RotatingShiftTypeCreateFormView(DynamicRotatingShiftTypeFormView): + """ + form view + """ + + is_dynamic_create_view = False + template_name = "cbv/rotating_shift/rot_shift_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = self.form_class() + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Rotating Shift Type") + context[form] = form + return context + + def form_valid(self, form: RotatingShiftForm) -> HttpResponse: + if form.is_valid(): + form.save() + if self.form.instance.pk: + message = _("Rotating Shift Updated") + else: + message = _("Rotating Shift Created") + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.add_rotatingshiftassign"), name="dispatch") +class RotatingShiftFormView(HorillaFormView): + """ + Create and edit form + """ + + model = RotatingShiftAssign + form_class = RotatingShiftAssignForm + new_display_title = _("Rotating Shift Assign") + dynamic_create_fields = [("rotating_shift_id", DynamicRotatingShiftTypeFormView)] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].initial = employee + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, self.form, "base.add_rotatingshiftassign" + ) + if self.form.instance.pk: + self.form_class.verbose_name = _("Rotating Shift Assign Update") + context["form"] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Rotating Shift Assign Update") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: RotatingShiftAssignForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Rotating Shift Assign Updated Successfully") + else: + message = _("Rotating Shift Assign Created Successfully") + employee_ids = self.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] + with contextlib.suppress(Exception): + notify.send( + self.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"), + ) + form.save() + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter("base.view_rotatingshiftassign"), name="dispatch") +class RotatingShiftAssignDuplicate(HorillaFormView): + """ + Duplicate form view + """ + + model = RotatingShiftAssign + form_class = RotatingShiftAssignForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = RotatingShiftAssign.objects.get(id=self.kwargs["pk"]) + form = self.form_class(instance=original_object) + context["form"] = form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + self.form_class.verbose_name = _("Duplicate") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: RotatingShiftAssignForm) -> HttpResponse: + form = self.form_class(self.request.POST) + self.form_class.verbose_name = _("Duplicate") + if form.is_valid(): + message = _("Rotating Shift Assign Created Successfully") + messages.success(self.request, message) + form.save() + return self.HttpResponse() + return self.form_invalid(form) diff --git a/base/cbv/rotating_work_type.py b/base/cbv/rotating_work_type.py new file mode 100644 index 000000000..5aed8ab85 --- /dev/null +++ b/base/cbv/rotating_work_type.py @@ -0,0 +1,451 @@ +""" +this page is handling the cbv methods of rotating work request page +""" + +from typing import Any + +from django.http import HttpResponse +from django.contrib import messages +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from notifications.signals import notify +from base.filters import RotatingWorkTypeAssignFilter +from base.forms import ( + RotatingWorkTypeAssignExportForm, + RotatingWorkTypeAssignForm, + RotatingWorkTypeForm, +) +from base.methods import ( + check_manager, + choosesubordinates, + filtersubordinates, + is_reportingmanager, +) +from base.models import RotatingWorkType, RotatingWorkTypeAssign +from base.decorators import manager_can_enter +from employee.models import Employee +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaNavView, + TemplateView, + HorillaListView, +) + + +@method_decorator( + manager_can_enter("base.view_rotatingworktypeassign"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class RotatingWorkRequestView(TemplateView): + """ + for page view + """ + + template_name = "cbv/rotating_work_type/rotating_work_home.html" + + +@method_decorator(login_required, name="dispatch") +class GeneralParent(HorillaListView): + """ + main parent class for list view + """ + + template_name = "cbv/rotating_work_type/extended_rotating_work.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-list-view") + self.view_id = "rotating-work-container" + if ( + self.request.user.has_perm("base.change_rotatingworktypeassign") + or self.request.user.has_perm("base.delete_rotatingworktypeassign") + or is_reportingmanager(self.request) + ): + self.action_method = "get_actions" + else: + self.action_method = None + + filter_class = RotatingWorkTypeAssignFilter + model = RotatingWorkTypeAssign + # filter_keys_to_remove = ["deleted"] + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Rotating Work Type"), "rotating_work_type_id"), + (_("Based On"), "get_based_on_display"), + (_("Rotate"), "rotate_data"), + (_("Start Date"), "start_date"), + (_("Current Work Type"), "current_work_type"), + (_("Next Switch"), "next_change_date"), + (_("Next Work Type"), "next_work_type"), + ] + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Rotating Work Type", "rotating_work_type_id__name"), + ("Based On", "get_based_on_display"), + ("Rotate", "rotate_data"), + ("Start Date", "start_date"), + ("Current Work Type", "current_work_type__work_type"), + ("Next Switch", "next_change_date"), + ("Next Work Type", "next_work_type__work_type"), + ] + row_attrs = """ + hx-get='{work_rotate_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator( + manager_can_enter("base.view_rotatingworktypeassign"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class RotatingWorkListView(GeneralParent): + """ + list view of the rotating work assign page + """ + + def get_queryset(self): + queryset = super().get_queryset() + queryset = filtersubordinates( + self.request, queryset, "base.view_rotatingworktypeassign" + ) + return queryset + + bulk_update_fields = [ + "rotating_work_type_id", + "start_date", + # "based_on" + ] + + +@method_decorator( + manager_can_enter("base.view_rotatingworktypeassign"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class RotatingWorkNavView(HorillaNavView): + """ + Nav view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-list-view") + self.actions = [] + if self.request.user.has_perm( + "base.change_rotatingworktypeassign" + ) or is_reportingmanager(self.request): + self.actions.append( + { + "action": "Archive", + "attrs": """ + id="archiveWorkRotateNav" + style="cursor: pointer;" + """, + } + ) + self.actions.append( + { + "action": _("Un-Archive"), + "attrs": """ + onclick=" + UnarchiveWorkRotateNav(); + " + style="cursor: pointer;" + """, + }, + ) + if self.request.user.has_perm( + "base.delete_rotatingworktypeassign" + ) or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + deleteWorkRotateNav(); + " + data-action ="delete" + style="cursor: pointer; color:red !important" + """, + } + ) + + if self.request.user.has_perm( + "base.view_rotatingworktypeassign" + ) or is_reportingmanager(self.request): + self.actions.insert( + 0, + { + "action": _("Export"), + "attrs": f""" + data-toggle = "oh-modal-toggle" + data-target = "#genericModal" + hx-target="#genericModalBody" + hx-get ="{reverse('rotating-action-export')}" + style="cursor: pointer;" + """, + }, + ) + if self.request.user.has_perm( + "base.add_rotatingworktypeassign" + ) or is_reportingmanager(self.request): + self.create_attrs = f""" + hx-get="{reverse_lazy("rotating-work-type-assign-add")}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + nav_title = _("Rotating Work Type Assign") + filter_body_template = "cbv/rotating_work_type/filter_work_rotate.html" + filter_instance = RotatingWorkTypeAssignFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("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_position_id", _("Job Role")), + ( + "employee_id__employee_work_info__reporting_manager_id", + _("Reporting Manager"), + ), + ] + + +@method_decorator( + manager_can_enter("base.view_rotatingworktypeassign"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class RotatingWorkDetailView(HorillaDetailedView): + """ + Detail view of page + """ + + model = RotatingWorkTypeAssign + + title = _("Details") + header = { + "title": "employee_id__get_full_name", + "subtitle": "work_rotate_detail_subtitle", + "avatar": "employee_id__get_avatar", + } + body = [ + (_("Title"), "rotating_work_type_id"), + (_("Based On"), "get_based_on_display"), + (_("Rotate"), "rotate_data"), + (_("Start Date"), "start_date"), + (_("Current Work Type"), "current_work_type"), + (_("Next Switch"), "next_change_date"), + (_("Next Work Type"), "next_work_type"), + (_("Status"), "detail_is_active"), + ] + action_method = "get_detail_view_actions" + + +@method_decorator( + manager_can_enter("base.view_rotatingworktypeassign"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class RotatingWorkExport(TemplateView): + """ + To add export action in the navbar + """ + + template_name = "cbv/rotating_work_type/rotating_action_export.html" + + def get_context_data(self, **kwargs: Any): + """ + get data for export + """ + candidates = RotatingWorkTypeAssign.objects.all() + export_columns = RotatingWorkTypeAssignExportForm + export_filter = RotatingWorkTypeAssignFilter(queryset=candidates) + context = super().get_context_data(**kwargs) + context["export_columns"] = export_columns + context["export_filter"] = export_filter + return context + + +@method_decorator(manager_can_enter("base.add_rotatingworktype"), name="dispatch") +@method_decorator(login_required, name="dispatch") +class DynamicRotatingWorkTypeCreate(HorillaFormView): + """ + form view for creating dynamic rotating work type + """ + + model = RotatingWorkType + form_class = RotatingWorkTypeForm + template_name = "cbv/rotating_work_type/forms/inherit.html" + new_display_title = _("Create Rotating Work Type") + is_dynamic_create_view = True + + def form_valid(self, form: RotatingWorkTypeForm) -> HttpResponse: + if form.is_valid(): + form.save() + messages.success(self.request, _("Rotating Work Type Created")) + return self.HttpResponse("") + + return self.form_invalid(form) diff --git a/base/cbv/settings_rotatingwork.py b/base/cbv/settings_rotatingwork.py new file mode 100644 index 000000000..312c1f73e --- /dev/null +++ b/base/cbv/settings_rotatingwork.py @@ -0,0 +1,103 @@ +""" +this page is handling the cbv methods for Rotating work type in settings +""" + +from typing import Any +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import RotatingWorkTypeFilter +from base.models import RotatingWorkType +from horilla.decorators import permission_required +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import HorillaListView, HorillaNavView + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.view_rotatingworktype"), name="dispatch") +class RotatingWorkTypeList(HorillaListView): + """ + list view of Rotating work types in settings + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-list") + self.actions = [] + if self.request.user.has_perm("base.change_rotatingworktype"): + self.actions.append( + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + }, + ) + if self.request.user.has_perm("base.delete_rotatingworktype"): + self.actions.append( + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.rotatingworktype&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = RotatingWorkType + filter_class = RotatingWorkTypeFilter + + row_attrs = """ + id="rotatingWorkTypeTr{get_delete_instance}" + """ + + columns = [ + (_("Title"), "name"), + (_("Work Type 1"), "work_type1"), + (_("Work Type 2"), "work_type2"), + (_("Additional Work Types"), "get_additional_worktytpes"), + ] + + sortby_mapping = [ + ("Title", "name"), + ("Work Type 1", "work_type1__work_type"), + ("Work Type 2", "work_type2__work_type"), + ("Additional Work Types", "get_additional_worktytpes"), + ] + + header_attrs = { + "name": """ style="width:200px !important" """, + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.view_rotatingworktype"), name="dispatch") +class RotatingWorkTypeNav(HorillaNavView): + """ + navbar of Rotating worktype + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("rotating-list") + if self.request.user.has_perm("base.add_rotatingworktype"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('rotating-work-type-create-form')}" + """ + + nav_title = _("Rotating Work Type") + search_swap_target = "#listContainer" + filter_instance = RotatingWorkTypeFilter() diff --git a/base/cbv/settings_work_type.py b/base/cbv/settings_work_type.py new file mode 100644 index 000000000..172d96ebb --- /dev/null +++ b/base/cbv/settings_work_type.py @@ -0,0 +1,94 @@ +""" +this page is handling the cbv methods for work type in settings +""" + +from typing import Any +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from base.filters import WorkTypeFilter +from base.models import WorkType +from horilla.decorators import permission_required +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import HorillaListView, HorillaNavView + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.view_worktype"), name="dispatch") +class WorkTypeList(HorillaListView): + """ + list view of work types in settings + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("worktype-list") + self.actions = [] + if self.request.user.has_perm("base.change_worktype"): + self.actions.append( + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ) + if self.request.user.has_perm("base.delete_worktype"): + self.actions.append( + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=base.worktype&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + } + ) + + model = WorkType + filter_class = WorkTypeFilter + + columns = [ + (_("Work Type"), "work_type"), + ] + + row_attrs = """ + id="workTypeTr{get_delete_instance}" + """ + + header_attrs = { + "work_type": """ style="width:300px !important" """, + "action": """ style="width:180px !important" """, + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.view_worktype"), name="dispatch") +class WorkTypeNav(HorillaNavView): + """ + navbar of worktype + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("worktype-list") + if self.request.user.has_perm("base.add_worktype"): + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('work-type-create-form')}" + """ + + nav_title = _("Work Type") + search_swap_target = "#listContainer" + filter_instance = WorkTypeFilter() diff --git a/base/cbv/shift_request.py b/base/cbv/shift_request.py new file mode 100644 index 000000000..96a1c5ab6 --- /dev/null +++ b/base/cbv/shift_request.py @@ -0,0 +1,666 @@ +""" +This page is handling the cbv methods of shift request page. +""" + +import contextlib +from typing import Any +from django import forms +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from django.db.models import Q +from employee.models import Employee +from notifications.signals import notify +from base.filters import ShiftRequestFilter +from base.forms import ( + EmployeeShiftForm, + ShiftAllocationForm, + ShiftRequestColumnForm, + ShiftRequestForm, +) +from base.models import EmployeeShift, ShiftRequest +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from base.views import include_employee_instance +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaTabView, + TemplateView, + HorillaNavView, + HorillaListView, + HorillaDetailedView, +) + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestView(TemplateView): + """ + Shift request page + """ + + template_name = "cbv/shift_request/shift_request.html" + + +@method_decorator(login_required, name="dispatch") +class ShiftList(HorillaListView): + """ + List view + """ + + model = ShiftRequest + filter_class = ShiftRequestFilter + + row_status_class = ( + "approved-{approved} canceled-{canceled} requested-{approved}-{canceled}" + ) + records_per_page = 5 + + row_status_indications = [ + ( + "canceled--dot", + _("Canceled"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=canceled]').val('true'); + $('[name=approved]').val('unknown').change(); + $('[name=requested]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "approved--dot", + _("Approved"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=approved]').val('true'); + $('[name=canceled]').val('unknown').change(); + $('[name=requested]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "requested--dot", + _("Requested"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=requested]').val('true'); + $('[name=approved]').val('unknown').change(); + $('[name=canceled]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ] + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestList(ShiftList): + """ + List view for shift requests + """ + + selected_instances_key_id = "shiftselectedInstances" + template_name = "cbv/shift_request/extended_shift.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-shift-request") + self.view_id = "shift-container" + # self.filter_keys_to_remove = ["deleted"] + if self.request.user.has_perm( + "base.change_shiftrequest" + ) or is_reportingmanager(self.request): + self.action_method = "confirmations" + else: + self.action_method = None + + def get_queryset(self): + queryset = super().get_queryset() + data = queryset + if not self.request.user.has_perm("base.view_shiftrequest"): + employee = self.request.user.employee_get + queryset = filtersubordinates( + self.request, + queryset.filter(reallocate_to__isnull=True), + "base.add_shiftrequest", + ) + queryset = queryset | data.filter(employee_id=employee) + queryset = queryset.filter(employee_id__is_active=True) + + return queryset + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Requested Shift"), "shift_id"), + (_("Previous/Current Shift"), "previous_shift_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + (_("Comment"), "comment"), + ] + header_attrs = { + "option": """ + style="width:190px !important;" + """ + } + + option_method = "shift_actions" + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name"), + ("Requested Shift", "shift_id__employee_shift"), + ("Previous/Current Shift", "previous_shift_id__employee_shift"), + ("Requested Date", "requested_date"), + ("Requested Till", "requested_till"), + ] + + row_attrs = """ + hx-get='{shift_details}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +class AllocatedShift(ShiftList): + """ + Allocated tab class + """ + + selected_instances_key_id = "allocatedselectedInstances" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("allocated-shift-view") + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Allocated Employee"), "reallocate_to"), + (_("User Availability"), "user_availability"), + (_("Requested Shift"), "shift_id"), + (_("Previous/Current Shift"), "previous_shift_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + (_("Comment"), "comment"), + ] + + action_method = "allocated_confirm_action_col" + option_method = "allocate_confirmations" + + def get_queryset(self): + queryset = super().get_queryset() + b = queryset + employee = self.request.user.employee_get + queryset = filtersubordinates( + self.request, + queryset.filter(reallocate_to__isnull=False), + "base.add_shiftrequest", + ) + allocated_requests = b.filter(reallocate_to__isnull=False) + if not self.request.user.has_perm("base.view_shiftrequest"): + allocated_requests = allocated_requests.filter( + Q(reallocate_to=employee) | Q(employee_id=employee) + ) + queryset = queryset | allocated_requests + + return queryset + + row_attrs = """ + hx-get='{allocate_shift_details}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + sortby_mapping = [ + ("Employee", "employee_id__get_full_name"), + ("Allocated Employee", "reallocate_to__get_full_name"), + ("Requested Shift", "shift_id__employee_shift"), + ("Previous/Current Shift", "previous_shift_id__employee_shift"), + ("Requested Date", "requested_date"), + ("Requested Till", "requested_till"), + ] + + +@method_decorator(login_required, name="dispatch") +class ShitRequestNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("shift-request-tab") + self.create_attrs = f""" + hx-get="{reverse_lazy('shift-request')}" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + + """ + self.actions = [] + if self.request.user.has_perm( + "base.change_shiftrequest" + ) or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Approve Requests"), + "attrs": """ + onclick=" + shiftRequestApprove(); + " + style="cursor: pointer;" + """, + } + ) + self.actions.append( + { + "action": _("Reject Requests"), + "attrs": """ + onclick=" + shiftRequestReject(); + " + style="cursor: pointer;" + """, + } + ) + if self.request.user.has_perm( + "base.delete_shiftrequest" + ) or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + shiftRequestDelete(); + " + data-action ="delete" + style="cursor: pointer; color:red !important" + """, + } + ) + + if self.request.user.has_perm("base.view_shiftrequest") or is_reportingmanager( + self.request + ): + self.actions.insert( + 0, + { + "action": _("Export"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('shift-export')}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + }, + ) + + nav_title = _("Shift Requests") + filter_body_template = "cbv/shift_request/filter.html" + filter_instance = ShiftRequestFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("employee_id", _("Employee")), + ("shift_id", _("Requested Shift")), + ("previous_shift_id", _("Current Shift")), + ("requested_date", _("Requested Date")), + ] + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestTab(HorillaTabView): + """ + Tab View + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "shift-tab" + self.tabs = [ + { + "title": _("Shift Requests"), + "url": f"{reverse('list-shift-request')}", + }, + { + "title": _("Allocated Shift Requests"), + "url": f"{reverse('allocated-shift-view')}", + }, + ] + + +@method_decorator(login_required, name="dispatch") +class ExportView(TemplateView): + """ + For candidate export + """ + + template_name = "cbv/shift_request/export_shift.html" + + def get_context_data(self, **kwargs: Any): + context = super().get_context_data(**kwargs) + shift_requests = ShiftRequest.objects.all() + export_fields = ShiftRequestColumnForm + export_filter = ShiftRequestFilter(queryset=shift_requests) + context["export_fields"] = export_fields + context["export_filter"] = export_filter + return context + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestDetailview(HorillaDetailedView): + """ + Detail View + """ + + model = ShiftRequest + + title = _("Details") + + header = { + "title": "employee_id__get_full_name", + "subtitle": "details_subtitle", + "avatar": "employee_id__get_avatar", + } + + body = [ + (_("Requested Shift"), "shift_id"), + (_("Previous Shift"), "previous_shift_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + (_("Is permenent shift"), "is_permanent"), + ] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + if self.request.GET.get("dashboard"): + self.action_method = "confirmations" + else: + self.action_method = "detail_actions" + + +@method_decorator(login_required, name="dispatch") +class AllocatedShiftDetailView(ShiftRequestDetailview): + """ + Allocated detail View + """ + + body = [ + (_("Allocated Employee"), "reallocate_to"), + (_("User Availability"), "user_availability"), + (_("Requested Shift"), "shift_id"), + (_("Previous Shift"), "previous_shift_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + ] + + +class ShiftTypeFormView(HorillaFormView): + """ + form view + """ + + model = EmployeeShift + form_class = EmployeeShiftForm + new_display_title = "Create Shift" + is_dynamic_create_view = True + + def form_valid(self, form: EmployeeShiftForm) -> HttpResponse: + if form.is_valid(): + form.save() + message = _("Shift Created") + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="base.add_employeeshift"), name="dispatch") +class ShiftTypeCreateFormView(ShiftTypeFormView): + + is_dynamic_create_view = False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # form = self.form_class() + if self.form.instance.pk: + self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Shift") + context["form"] = self.form + return context + + def form_valid(self, form: EmployeeShiftForm) -> HttpResponse: + if form.is_valid(): + if self.form.instance.pk: + message = _("Shift Updated") + else: + message = _("Shift Created") + form.save() + messages.success(self.request, message) + return self.HttpResponse() + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestFormView(HorillaFormView): + """ + Form View + """ + + model = ShiftRequest + form_class = ShiftRequestForm + new_display_title = _("Create Shift Request") + template_name = "cbv/shift_request/shift_request_form.html" + dynamic_create_fields = [("shift_id", ShiftTypeFormView)] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + employee = self.request.user.employee_get.id + if self.form.instance.pk: + self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, + self.form, + "base.add_shiftrequest", + ) + self.form = include_employee_instance(self.request, self.form) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + self.form.fields["employee_id"].initial = employee + context["form"] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: ShiftRequestForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Shift request updated Successfully") + form.save() + else: + instance = form.save() + message = _("Shift request added Successfully") + with contextlib.suppress(Exception): + 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}", + ) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class ShiftRequestFormDuplicate(HorillaFormView): + """ + Duplicate form view + """ + + model = ShiftRequest + form_class = ShiftRequestForm + template_name = "cbv/shift_request/shift_request_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = ShiftRequest.objects.get(id=self.kwargs["pk"]) + self.form = self.form_class(instance=original_object) + for field_name, field in self.form.fields.items(): + if isinstance(field, forms.CharField): + if field.initial: + initial_value = field.initial + else: + initial_value = f"{self.form.initial.get(field_name, '')} (copy)" + self.form.initial[field_name] = initial_value + self.form.fields[field_name].initial = initial_value + self.form = choosesubordinates( + self.request, + self.form, + "base.add_shiftrequest", + ) + self.form = include_employee_instance(self.request, self.form) + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + context["form"] = self.form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + # form = self.form_class(self.request.POST) + self.form_class.verbose_name = _("Duplicate") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: ShiftRequestForm) -> HttpResponse: + form = self.form_class(self.request.POST) + if form.is_valid(): + form.save() + message = _("Shift request added Successfully") + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class ShiftAllocationFormView(HorillaFormView): + """ + Form View + """ + + model = ShiftRequest + form_class = ShiftAllocationForm + new_display_title = _("Shift Request") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, + self.form, + "base.add_shiftrequest", + ) + self.form = include_employee_instance(self.request, self.form) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].initial = employee + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + context["form"] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + + # form = self.form_class(self.request.POST) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: ShiftRequestForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Shift request updated Successfully") + form.save() + else: + instance = form.save() + message = _("Shift request added Successfully") + reallocate_emp = form.cleaned_data["reallocate_to"] + with contextlib.suppress(Exception): + notify.send( + form.instance.employee_id, + recipient=( + form.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}", + ) + + notify.send( + instance.employee_id, + recipient=(reallocate_emp.employee_user_id), + 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}", + ) + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) diff --git a/base/cbv/work_shift_tab.py b/base/cbv/work_shift_tab.py new file mode 100644 index 000000000..867a31531 --- /dev/null +++ b/base/cbv/work_shift_tab.py @@ -0,0 +1,277 @@ +""" +This page is handling the cbv methods of work type and shift tab in employee profile page. +""" + +from typing import Any +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.urls import reverse +from base.cbv.rotating_shift_assign import ( + RotatingShiftDetailview, + RotatingShiftListParent, +) +from base.cbv.rotating_work_type import GeneralParent, RotatingWorkDetailView +from base.cbv.work_type_request import WorkRequestListView +from base.cbv.shift_request import ShiftRequestList +from base.methods import filtersubordinates, is_reportingmanager +from base.models import WorkTypeRequest +from employee.models import Employee +from horilla_views.generic.cbv.views import HorillaTabView +from horilla_views.cbv_methods import login_required + + +class WorkAndShiftTabView(HorillaTabView): + """ + generic tab view for work type and shift + """ + + template_name = "cbv/work_shift_tab/extended_work-shift.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "work-shift" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + context["emp_id"] = pk + employee = Employee.objects.get(id=pk) + context["employee"] = employee + context["tabs"] = [ + { + "title": _("Work type request"), + "url": f"{reverse('employee-worktype-tab-list',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Add Work Type Request", + "attrs": f""" + hx-get="{reverse('work-type-request')}?emp_id={pk}", + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ], + }, + { + "title": _("Rotating work type"), + "url": f"{reverse('employee-rotating-work-tab-list',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Add Rotating Work", + "attrs": f""" + hx-get="{reverse('rotating-work-type-assign-add')}?emp_id={pk}", + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ], + }, + { + "title": _("Shift request"), + "url": f"{reverse('shift-request-individual-tab-view',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Add Shift Request", + "attrs": f""" + hx-get="{reverse('shift-request')}?emp_id={pk}", + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ], + }, + { + "title": _("Rotating Shift"), + "url": f"{reverse('rotating-shift-individual-tab-view',kwargs={'pk': pk})}", + "actions": [ + { + "action": "Add Rotating Shift", + "attrs": f""" + hx-get="{reverse('rotating-shift-assign-add')}?emp_id={pk}", + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + } + ], + }, + ] + return context + + +class WorkTypeIndividualTabList(WorkRequestListView): + """ + List view for work type tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("employee-worktype-tab-list",kwargs= {'pk': pk} ) + self.view_id = "work_target" + + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = WorkTypeRequest.objects.filter(employee_id=pk) + return queryset + + columns = [ + col for col in WorkRequestListView.columns if col[1] != "comment_note" + ] + [(_("Status"), "request_status")] + + +class ShiftRequestIndividualTabView(ShiftRequestList): + """ + List view for shift request tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.view_id = "shift-reques-individual-div" + self.selected_instances_key_id = "shiftselectedInstancesIndividual" + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("shift-request-individual-tab-view",kwargs= {'pk': pk} ) + + columns = [ + column for column in ShiftRequestList.columns if column[1] != "comment" + ] + [(_("Status"), "request_status")] + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset + + +class RotatingShiftAssignIndividualView(RotatingShiftListParent): + """ + List view for Rotating shift request tab + """ + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("rotating-shift-individual-tab-view",kwargs= {'pk': pk} ) + self.view_id = "rotating-div" + + + + columns = RotatingShiftListParent.columns + [ + (_("Status"), "check_active"), + ] + + row_attrs = """ + hx-get='{rotating_shift_individual_detail}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset + + +class RotatingWorkIndividualTab(GeneralParent): + """ + List view for rotating work type tab + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + pk = self.request.resolver_match.kwargs.get('pk') + self.search_url = reverse("employee-rotating-work-tab-list",kwargs= {'pk': pk} ) + self.view_id = "rotating-work-div" + + columns = GeneralParent.columns + [ + (_("Status"), "detail_is_active"), + ] + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + queryset = queryset.filter(employee_id=pk) + return queryset + + row_attrs = """ + hx-get='{individual_tab_work_rotate_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +# @method_decorator(login_required, name="dispatch") +# class DetailViewChild(RotatingWorkDetailView): +# """ +# parent for detail view +# """ + +# @method_decorator(login_required, name="dispatch") +# def dispatch(self, *args, **kwargs): +# return super(RotatingWorkDetailView, self).dispatch(*args, **kwargs) + +# def get_queryset(self): +# queryset = super().get_queryset() +# pk = self.kwargs.get("pk") +# queryset = queryset.filter(pk=pk) +# return queryset + + +class RotatingShiftAssignIndividualDetailView(RotatingShiftDetailview): + """ + Individual rotating shift assign detail view + """ + + @method_decorator(login_required, name="dispatch") + def dispatch(self, *args, **kwargs): + return super(RotatingShiftDetailview, self).dispatch(*args, **kwargs) + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + obj = queryset.get(pk=pk) + employee_id = obj.employee_id + if is_reportingmanager(self.request): + queryset = filtersubordinates( + self.request, queryset, "base.view_rotatingshiftassign" + ) | queryset.filter(employee_id=self.request.user.employee_get) + elif self.request.user.has_perm("base.view_rotatingshiftassign"): + queryset = queryset.filter(employee_id=employee_id) + else: + queryset = queryset.filter(employee_id=self.request.user.employee_get) + return queryset + + +@method_decorator(login_required, name="dispatch") +class DetailViewChild(RotatingWorkDetailView): + """ + parent for detail view + """ + + @method_decorator(login_required, name="dispatch") + def dispatch(self, *args, **kwargs): + return super(RotatingWorkDetailView, self).dispatch(*args, **kwargs) + + def get_queryset(self): + queryset = super().get_queryset() + pk = self.kwargs.get("pk") + obj = queryset.get(pk=pk) + emp_id = obj.employee_id + # queryset = queryset.filter(employee_id=emp_id) + if is_reportingmanager(self.request): + queryset = filtersubordinates( + self.request, queryset, "base.view_rotatingworktypeassign" + ) | queryset.filter(employee_id=self.request.user.employee_get) + elif self.request.user.has_perm("base.view_rotatingworktypeassign"): + queryset = queryset.filter(employee_id=emp_id) + else: + queryset = queryset.filter(employee_id=self.request.user.employee_get) + + return queryset diff --git a/base/cbv/work_type_request.py b/base/cbv/work_type_request.py new file mode 100644 index 000000000..b112932b5 --- /dev/null +++ b/base/cbv/work_type_request.py @@ -0,0 +1,492 @@ +""" +this page is handling the cbv methods of work request page +""" + +import contextlib +from typing import Any +from django import forms +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from employee.models import Employee +from notifications.signals import notify +from django.utils.translation import gettext_lazy as _ +from base.filters import WorkTypeRequestFilter +from base.forms import WorkTypeForm, WorkTypeRequestColumnForm, WorkTypeRequestForm +from base.methods import choosesubordinates, filtersubordinates, is_reportingmanager +from base.models import WorkType, WorkTypeRequest +from base.views import include_employee_instance +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + TemplateView, + HorillaListView, + HorillaNavView, + HorillaFormView, +) +from horilla_views.cbv_methods import login_required, permission_required + + +@method_decorator(login_required, name="dispatch") +class WorkRequestView(TemplateView): + """ + for page view + """ + + template_name = "cbv/work_type_request/work_type_home.html" + + +class WorkRequestListView(HorillaListView): + """ + list view of the work request page + """ + + filter_class = WorkTypeRequestFilter + model = WorkTypeRequest + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("work-list-view") + self.view_id = "work-shift" + if self.request.user.has_perm( + "base.change_worktyperequest" + ) or is_reportingmanager(self.request): + self.action_method = "confirmation" + else: + self.action_method = None + + def get_queryset(self): + """ + queryset to filter data based on permission + """ + queryset = super().get_queryset() + view_data = queryset + if not self.request.user.has_perm("base.view_worktyperequest"): + employee = self.request.user.employee_get + queryset = filtersubordinates( + self.request, queryset, "base.add_worktyperequest" + ) + queryset = queryset | view_data.filter(employee_id=employee) + queryset = queryset.filter(employee_id__is_active=True) + return queryset + + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Requested Work Type"), "work_type_id"), + (_("Previous/current Work Type"), "previous_work_type_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + (_("Comment"), "comment_note"), + ] + records_per_page = 10 + option_method = "work_actions" + + header_attrs = { + "option": """ + style="width:200px !important;" + """, + } + + sortby_mapping = [ + ("Employee", "employee_id__get_full_name", "employee_id__get_avatar"), + ("Requested Work Type", "work_type_id__work_type"), + ("Previous/current Work Type", "previous_work_type_id__work_type"), + ("Requested Date", "requested_date"), + ("Requested Till", "requested_till"), + ] + row_attrs = """ + hx-get='{detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + row_status_class = ( + "approved-{approved} canceled-{canceled} requested-{approved}-{canceled}" + ) + row_status_indications = [ + ( + "canceled--dot", + _("Rejected"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=canceled]').val('true'); + $('[name=approved]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "approved--dot", + _("Approved"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=approved]').val('true'); + $('[name=canceled]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ( + "requested--dot", + _("Requested"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=requested]').val('true'); + $('[name=canceled]').val('unknown').change(); + $('[name=approved]').val('unknown').change(); + $('#applyFilter').click(); + " + """, + ), + ] + + +@method_decorator(login_required, name="dispatch") +class WorkRequestNavView(HorillaNavView): + """ + nav view of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("work-list-view") + self.actions = [] + if self.request.user.has_perm("base.change_worktyperequest") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Approve Requests"), + "attrs": """ + onclick=" + handleApproveRequestsClick(); + " + style="cursor: pointer;" + """, + } + ) + self.actions.append( + { + "action": _("Reject Requests"), + "attrs": """ + onclick=" + handleRejectRequestsClick(); + " + style="cursor: pointer;" + """ + } + ) + if self.request.user.has_perm("base.delete_worktyperequest") or is_reportingmanager(self.request): + self.actions.append( + { + "action": _("Delete"), + "attrs": """ + onclick=" + handleDeleteRequestsClick(); + " + data-action ="delete" + style="cursor: pointer; color:red !important" + + """, + } + ) + if self.request.user.has_perm("base.view_worktyperequest") or is_reportingmanager(self.request): + self.actions.insert( + 0, + { + "action": _("Export"), + "attrs": f""" + hx-target = "#genericModalBody" + data-toggle = "oh-modal-toggle" + data-target = "#genericModal" + hx-get ="{reverse('work-export-candidate')}" + style="cursor: pointer;" + """, + }, + ) + + self.create_attrs = f""" + hx-get="{reverse_lazy("work-type-request")}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + nav_title = _("Work Type Requests") + filter_body_template = "cbv/work_type_request/filter.html" + filter_instance = WorkTypeRequestFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + group_by_fields = [ + ("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"), + ), + ] + + +@method_decorator(login_required, name="dispatch") +class WorkTypeDetailView(HorillaDetailedView): + """ + Detail view of page + """ + + model = WorkTypeRequest + title = _("Details") + header = { + "title": "employee_id__get_full_name", + "subtitle": "detail_subtitle", + "avatar": "employee_id__get_avatar", + } + body = [ + (_("Requested Work Type"), "work_type_id"), + (_("Previous Work Type"), "previous_work_type_id"), + (_("Requested Date"), "requested_date"), + (_("Requested Till"), "requested_till"), + (_("Description"), "description"), + (_("Is Permanent Work Type"), "is_permanent_work_type_display"), + ] + + action_method = "confirmation" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + if self.request.GET.get("dashboard"): + self.action_method = "confirmation" + else: + self.action_method = "detail_view_actions" + + +@method_decorator(login_required, name="dispatch") +class WorkExportCandidate(TemplateView): + """ + view for Export candidates + """ + + template_name = "cbv/work_type_request/work_export.html" + + def get_context_data(self, **kwargs: Any): + """ + get data for export + """ + candidates = WorkTypeRequest.objects.all() + export_fields = WorkTypeRequestColumnForm() + export_filter = WorkTypeRequestFilter(queryset=candidates) + context = super().get_context_data(**kwargs) + context["export_fields"] = export_fields + context["export_filter"] = export_filter + return context + + +@method_decorator(login_required, name="dispatch") +class DynamicWorkTypeCreateForm(HorillaFormView): + """ + form view for creating dynamic work types + """ + + model = WorkType + form_class = WorkTypeForm + new_display_title = _("Create Work Type") + is_dynamic_create_view = True + + def form_valid(self, form: WorkTypeForm) -> HttpResponse: + if form.is_valid(): + form.save() + messages.success(self.request, _("New Work Type Created")) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("base.add_worktype"), name="dispatch") +class WorkTypesCreateForm(DynamicWorkTypeCreateForm): + """ + form view for creating work types on settings + """ + + is_dynamic_create_view = False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = self.form_class() + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Work Type") + context[form] = form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Work Type") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: WorkTypeForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + messages.success(self.request, _("Work Type Updated")) + else: + messages.success(self.request, _("New Work Type Created")) + form.save() + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class WorkTypeFormView(HorillaFormView): + """ + form view for creating work types in app + """ + + model = WorkTypeRequest + form_class = WorkTypeRequestForm + template_name = "cbv/work_type_request/form/form.html" + + new_display_title = _("Work Type Request") + dynamic_create_fields = [("work_type_id", DynamicWorkTypeCreateForm)] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + employee = self.request.user.employee_get.id + if self.form.instance.pk: + self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, + self.form, + "base.add_worktyperequest", + ) + self.form = include_employee_instance(self.request, self.form) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + self.form.fields["employee_id"].initial = employee + context["form"] = self.form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + + # form = self.form_class(self.request.POST) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Request") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: WorkTypeRequestForm) -> HttpResponse: + + if form.is_valid(): + if form.instance.pk: + message = _("Request Updated Successfully") + form.save() + else: + instance = form.save() + message = _("Work type Request Created") + with contextlib.suppress(Exception): + 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}", + ) + + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +class WorkTypeDuplicateForm(HorillaFormView): + """ + duplicate form + """ + + model = WorkTypeRequest + form_class = WorkTypeRequestForm + # template_name = "cbv/work_type_request/form/form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = WorkTypeRequest.objects.get(id=self.kwargs["pk"]) + self.form = self.form_class(instance=original_object) + + for field_name, field in self.form.fields.items(): + if isinstance(field, forms.CharField): + initial_value = self.form.initial.get(field_name, "") + if initial_value: + initial_value += " (copy)" + self.form.initial[field_name] = initial_value + self.form.fields[field_name].initial = initial_value + if self.form.instance.pk: + self.form_class(instance=self.form.instance) + self.form = choosesubordinates( + self.request, + self.form, + "base.add_worktyperequest", + ) + self.form = include_employee_instance(self.request, self.form) + if hasattr(self.form.instance, "id"): + self.form.instance.id = None + if self.request.GET.get("emp_id"): + employee = self.request.GET.get("emp_id") + self.form.fields["employee_id"].queryset = Employee.objects.filter( + id=employee + ) + + # context["form"] = form + context["form"] = self.form + self.form_class.verbose_name = _("Work Type Request") + return context + + def form_invalid(self, form: Any) -> HttpResponse: + # form = self.form_class(self.request.POST) + self.form_class.verbose_name = _("Work Type Request") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: WorkTypeRequestForm) -> HttpResponse: + form = self.form_class(self.request.POST) + if form.is_valid(): + form.save() + message = _("Work type Request Created") + messages.success(self.request, message) + return self.HttpResponse() + + return super().form_valid(form) diff --git a/base/decorators.py b/base/decorators.py index 9a2e20186..cb427f0a0 100644 --- a/base/decorators.py +++ b/base/decorators.py @@ -3,9 +3,13 @@ decorator functions for base """ from django.contrib import messages -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render -from .models import ShiftRequest, WorkTypeRequest +from employee.models import EmployeeWorkInformation +from horilla.horilla_middlewares import _thread_locals + +from .models import MultipleApprovalManagers, ShiftRequest, WorkTypeRequest decorator_with_arguments = ( lambda decorator: lambda *args, **kwargs: lambda func: decorator( @@ -66,3 +70,44 @@ def work_type_request_change_permission(function=None, *args, **kwargs): # return function(request, *args, **kwargs) return check_permission + + +@decorator_with_arguments +def manager_can_enter(function, perm): + """ + This method is used to check permission to employee for enter to the function if the employee + do not have permission also checks, has reporting manager. + """ + + def _function(self, *args, **kwargs): + leave_perm = [ + "leave.view_leaverequest", + "leave.change_leaverequest", + "leave.delete_leaverequest", + ] + request = getattr(_thread_locals, "request") + if not getattr(self, "request", None): + self.request = request + user = request.user + employee = user.employee_get + if perm in leave_perm: + is_approval_manager = MultipleApprovalManagers.objects.filter( + employee_id=employee.id + ).exists() + if is_approval_manager: + return function(self, *args, **kwargs) + is_manager = EmployeeWorkInformation.objects.filter( + reporting_manager_id=employee + ).exists() + if user.has_perm(perm) or is_manager: + return function(self, *args, **kwargs) + else: + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function diff --git a/base/filters.py b/base/filters.py index 65aa05837..171cbd870 100644 --- a/base/filters.py +++ b/base/filters.py @@ -7,22 +7,37 @@ import uuid import django_filters from django import forms +from django.db.models import Q from django.utils.translation import gettext as __ -from django_filters import CharFilter, DateFilter, FilterSet, NumberFilter, filters +from django_filters import CharFilter, DateFilter, FilterSet, filters from base.models import ( + Announcement, + AnnouncementView, + Company, CompanyLeaves, + Department, + DynamicEmailConfiguration, + EmailLog, + EmployeeShift, + EmployeeShiftSchedule, + EmployeeType, Holidays, + JobPosition, + MultipleApprovalCondition, PenaltyAccounts, + RotatingShift, RotatingShiftAssign, + RotatingWorkType, RotatingWorkTypeAssign, ShiftRequest, + WorkType, WorkTypeRequest, ) -from horilla.filters import FilterSet, filter_by_name +from horilla.filters import FilterSet, HorillaFilterSet, filter_by_name -class ShiftRequestFilter(FilterSet): +class ShiftRequestFilter(HorillaFilterSet): """ Custom filter for Shift Requests. """ @@ -42,6 +57,10 @@ class ShiftRequestFilter(FilterSet): ) search = CharFilter(method=filter_by_name) + requested = django_filters.BooleanFilter( + method="filter_requested", label="Requested?" + ) + class Meta: """ A nested class that specifies the model and fields for the filter. @@ -77,8 +96,16 @@ class ShiftRequestFilter(FilterSet): for field in self.form.fields.keys(): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + def filter_requested(self, queryset, name, value): + """ + Filters the queryset to return entries where 'approved' is False and 'canceled' is False. + """ + if value: + return queryset.filter(approved=False, canceled=False) + return queryset -class WorkTypeRequestFilter(FilterSet): + +class WorkTypeRequestFilter(HorillaFilterSet): """ Custom filter for Work Type Requests. """ @@ -96,6 +123,9 @@ class WorkTypeRequestFilter(FilterSet): lookup_expr="lte", widget=forms.DateInput(attrs={"type": "date"}), ) + requested = django_filters.BooleanFilter( + method="filter_by_requested", label="Requested" + ) search = CharFilter(method=filter_by_name) class Meta: @@ -132,8 +162,16 @@ class WorkTypeRequestFilter(FilterSet): for field in self.form.fields.keys(): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + def filter_by_requested(self, queryset, name, value): + """ + Filters the queryset to return entries where 'approved' is False and 'canceled' is False. + """ + if value: + return queryset.filter(approved=False, canceled=False) + return queryset -class RotatingShiftAssignFilters(FilterSet): + +class RotatingShiftAssignFilters(HorillaFilterSet): """ Custom filter for Rotating Shift Assign. """ @@ -177,7 +215,7 @@ class RotatingShiftAssignFilters(FilterSet): ] -class RotatingWorkTypeAssignFilter(FilterSet): +class RotatingWorkTypeAssignFilter(HorillaFilterSet): """ Custom filter for Rotating Work Type Assign. """ @@ -285,7 +323,185 @@ class RotatingShiftRequestReGroup: ] -class HolidayFilter(FilterSet): +class MultipleApprovalConditionFilter(HorillaFilterSet): + + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = MultipleApprovalCondition + fields = [ + "department", + ] + + def search_method(self, queryset, _, value): + """ + This method is used to search department + """ + + return (queryset.filter(department__department__icontains=value)).distinct() + + +class EmployeeShiftFilter(FilterSet): + + search = django_filters.CharFilter( + field_name="employee_shift", lookup_expr="icontains" + ) + + class Meta: + model = EmployeeShift + fields = [ + "employee_shift", + ] + + +class EmployeeShiftScheduleFilter(FilterSet): + + search = django_filters.CharFilter(field_name="day__day", lookup_expr="icontains") + + class Meta: + model = EmployeeShiftSchedule + fields = [] + + +class RotatingShiftFilter(HorillaFilterSet): + + # search = django_filters.CharFilter( + # field_name="name", lookup_expr="icontains" + # ) + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = RotatingShift + fields = ["name", "shift1", "shift2"] + + def search_method(self, queryset, _, value): + """ + This method is used to search employees and objective + """ + + return ( + queryset.filter(name__icontains=value) + | queryset.filter(shift1__employee_shift__icontains=value) + | queryset.filter(shift2__employee_shift__icontains=value) + ).distinct() + + +class DepartmentViewFilter(HorillaFilterSet): + search = django_filters.CharFilter(method="filter_by_all_fields") + + class Meta: + model = Department + fields = [ + "department", + ] + + def filter_by_all_fields(self, queryset, name, value): + return queryset.filter( + Q(department__icontains=value) + | Q(job_position__job_position__icontains=value) + ).distinct() + + +class WorkTypeFilter(HorillaFilterSet): + + search = django_filters.CharFilter(field_name="work_type", lookup_expr="icontains") + + class Meta: + model = WorkType + fields = [ + "work_type", + ] + + +class RotatingWorkTypeFilter(HorillaFilterSet): + + search = django_filters.CharFilter(method="search_method") + + def search_method(self, queryset, _, value): + """ + This method is used to search employees and objective + """ + + return ( + queryset.filter(name__icontains=value) + | queryset.filter(work_type1__work_type__icontains=value) + | queryset.filter(work_type2__work_type__icontains=value) + ).distinct() + + class Meta: + model = RotatingWorkType + fields = ["name", "work_type1", "work_type2"] + + +class EmployeeTypeFilter(FilterSet): + + search = django_filters.CharFilter( + field_name="employee_type", lookup_expr="icontains" + ) + + class Meta: + model = EmployeeType + fields = [ + "employee_type", + ] + + +class JobRoleFilter(HorillaFilterSet): + search = django_filters.CharFilter(method="filter_by_all_fields") + + class Meta: + model = JobPosition + fields = [ + "job_position", + ] + + def filter_by_all_fields(self, queryset, name, value): + return queryset.filter( + Q(job_position__icontains=value) | Q(jobrole__job_role__icontains=value) + ).distinct() + + +class CompanyFilter(FilterSet): + + search = CharFilter(method="search_method") + + def search_method(self, queryset, _, value): + """ + This method is used to search company and objective + """ + + return ( + queryset.filter(company__icontains=value) + | queryset.filter(hq__icontains=value) + | queryset.filter(address__icontains=value) + | queryset.filter(country__icontains=value) + | queryset.filter(state__icontains=value) + | queryset.filter(city__icontains=value) + | queryset.filter(zip__icontains=value) + ).distinct() + + class Meta: + model = Company + fields = ["company", "hq", "address", "country", "state", "city", "zip"] + + +class MailServerFilter(HorillaFilterSet): + + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = DynamicEmailConfiguration + fields = ["username"] + + def search_method(self, queryset, _, value): + """ + This method is used to mail server + """ + + return ((queryset.filter(username__icontains=value))).distinct() + + +class HolidayFilter(HorillaFilterSet): """ Filter class for Holidays model. @@ -338,7 +554,7 @@ class HolidayFilter(FilterSet): ) -class CompanyLeaveFilter(FilterSet): +class CompanyLeaveFilter(HorillaFilterSet): """ Filter class for CompanyLeaves model. @@ -402,3 +618,32 @@ class PenaltyFilter(FilterSet): class Meta: model = PenaltyAccounts fields = "__all__" + + +class MailLogFilter(HorillaFilterSet): + + search = django_filters.CharFilter(field_name="subject", lookup_expr="icontains") + + class Meta: + model = EmailLog + fields = "__all__" + + +class AnnouncementFilter(HorillaFilterSet): + + search = django_filters.CharFilter(field_name="title", lookup_expr="icontains") + + class Meta: + model = Announcement + fields = "__all__" + + +class AnnouncementViewFilter(HorillaFilterSet): + + search = django_filters.CharFilter( + field_name="announcement", lookup_expr="icontains" + ) + + class Meta: + model = AnnouncementView + fields = "__all__" diff --git a/base/forms.py b/base/forms.py index d10a67fc9..f7aa920d5 100644 --- a/base/forms.py +++ b/base/forms.py @@ -30,7 +30,6 @@ 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 ( @@ -68,9 +67,10 @@ from base.models import ( WorkTypeRequest, WorkTypeRequestComment, ) +from base.widgets import CustomModelChoiceWidget from employee.filters import EmployeeFilter from employee.forms import MultipleFileField -from employee.models import Employee +from employee.models import Employee, EmployeeTag from horilla import horilla_middlewares from horilla.horilla_middlewares import _thread_locals from horilla.methods import get_horilla_model_class @@ -98,9 +98,9 @@ def validate_time_format(value): BASED_ON = [ - ("after", _trans("After")), - ("weekly", _trans("Weekend")), - ("monthly", _trans("Monthly")), + ("after", _("After")), + ("weekly", _("Weekend")), + ("monthly", _("Monthly")), ] @@ -387,6 +387,30 @@ class AssignUserGroup(Form): return group +class AddToUserGroupForm(Form): + """ + Form to add employee in to groups + """ + + group = forms.ModelMultipleChoiceField(queryset=Group.objects.all(), required=False) + employee = forms.ModelChoiceField(queryset=Employee.objects.all()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + + def save(self): + """ + Save method to assign the selected groups to the employee + """ + employee = self.cleaned_data["employee"] + groups = self.cleaned_data["group"] + employee.employee_user_id.groups.clear() + for group in groups: + employee.employee_user_id.groups.add(group) + return employee + + class AssignPermission(Form): """ Forms to assign user permision @@ -445,6 +469,15 @@ class CompanyForm(ModelForm): Company model's form """ + cols = { + "company": 12, + "address": 12, + "country": 12, + "state": 12, + "city": 12, + "zip": 12, + } + class Meta: """ Meta class for additional options @@ -478,6 +511,8 @@ class DepartmentForm(ModelForm): Department model's form """ + cols = {"department": 12, "company_id": 12} + class Meta: """ Meta class for additional options @@ -493,6 +528,16 @@ class JobPositionForm(ModelForm): JobPosition model's form """ + department_id = forms.ModelMultipleChoiceField( + queryset=Department.objects.all(), + label="Department", + widget=forms.SelectMultiple( + attrs={"class": "oh-select oh-select2 w-100", "style": "height:45px;"} + ), + ) + + cols = {"job_position": 12, "department_id": 12} + class Meta: """ Meta class for additional options @@ -500,7 +545,74 @@ class JobPositionForm(ModelForm): model = JobPosition fields = "__all__" - exclude = ["is_active"] + exclude = ["is_active", "department_id", "company_id"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields["department_id"] = forms.ModelChoiceField( + queryset=self.fields["department_id"].queryset, + label="Department", + widget=forms.Select( + attrs={ + "class": "oh-select oh-select2 w-100", + "style": "height:45px;", + } + ), + ) + + def clean(self): + """ + Perform custom validation. + """ + cleaned_data = super().clean() + job_position = cleaned_data.get("job_position") + department_ids = cleaned_data.get("department_id") + + if department_ids and not hasattr(department_ids, "__iter__"): + department_ids = [department_ids] + + if self.instance.pk and job_position and department_ids: + for department_id in department_ids: + if ( + JobPosition.objects.filter( + department_id=department_id, job_position=job_position + ) + .exclude(pk=self.instance.pk) + .exists() + ): + raise ValidationError( + _( + f"Job position '{job_position}' already exists under department {department_id}" + ) + ) + + return cleaned_data + + def save(self, commit, *args, **kwargs) -> Any: + if not self.instance.pk: + request = getattr(_thread_locals, "request") + department = Department.objects.filter( + id__in=self.data.getlist("department_id") + ) + positions = [] + for dep in department: + position = JobPosition() + position.department_id = dep + position.job_position = self.data["job_position"] + form_data = self.data["job_position"] + if JobPosition.objects.filter( + department_id=dep, job_position=form_data + ).exists(): + messages.error(request, f"Job position already exists under {dep}") + else: + messages.success( + request, _("Job position has been created successfully!") + ) + position.save() + positions.append(position.pk) + return JobPosition.objects.filter(id__in=positions) + super().save(commit, *args, **kwargs) class JobPositionMultiForm(ModelForm): @@ -580,6 +692,19 @@ class JobRoleForm(ModelForm): JobRole model's form """ + cols = {"job_position_id": 12, "job_role": 12} + + job_position_id = forms.ModelMultipleChoiceField( + queryset=JobPosition.objects.all(), + label="Job Position", + widget=forms.SelectMultiple( + attrs={ + "class": "w-100", + "style": "height:45px;", + } + ), + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.instance.pk: @@ -587,9 +712,7 @@ class JobRoleForm(ModelForm): 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;" + self.fields["job_position_id"].initial = self.instance.job_position_id class Meta: """ @@ -598,7 +721,26 @@ class JobRoleForm(ModelForm): model = JobRole fields = "__all__" - exclude = ["is_active"] + exclude = ["is_active", "job_position_id", "company_id"] + + def clean(self): + cleaned_data = super().clean() + job_position_id = cleaned_data.get("job_position_id") + job_role = cleaned_data.get("job_role") + + if job_position_id and not hasattr(job_position_id, "__iter__"): + job_position_id = [job_position_id] + + if self.instance.pk and job_position_id and job_role: + existing_roles = JobRole.objects.filter( + job_position_id__in=job_position_id, job_role=job_role + ).exclude(pk=self.instance.pk) + if existing_roles.exists(): + raise ValidationError( + f"{job_role} already exists under this job position" + ) + + return cleaned_data def save(self, commit, *args, **kwargs) -> Any: if not self.instance.pk: @@ -613,6 +755,9 @@ class JobRoleForm(ModelForm): role.job_role = self.data["job_role"] try: role.save() + messages.success( + request, _("Job role has been created successfully!") + ) except: messages.info(request, f"Role already exists under {position}") roles.append(role.pk) @@ -625,6 +770,8 @@ class WorkTypeForm(ModelForm): WorkType model's form """ + cols = {"work_type": 12, "company_id": 12} + class Meta: """ Meta class for additional options @@ -640,6 +787,12 @@ class RotatingWorkTypeForm(ModelForm): RotatingWorkType model's form """ + cols = { + "name": 12, + "work_type1": 12, + "work_type2": 12, + } + class Meta: """ Meta class for additional options @@ -658,18 +811,21 @@ class RotatingWorkTypeForm(ModelForm): 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( + widget=CustomModelChoiceWidget( + delete_url="/add-remove-work-type-fields", attrs={ - "class": "oh-select oh-select-2 mb-3", + "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, + label="", ) for key in self.data.keys(): @@ -746,22 +902,30 @@ 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")) + cols = { + "employee_id": 12, + "rotating_work_type_id": 12, + "start_date": 12, + "based_on": 12, + "rotate_after_day": 12, + "rotate_every_weekend": 12, + "rotate_every": 12, + } + + # 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=_("Employees"), + # ) + based_on = forms.ChoiceField(choices=BASED_ON, initial="daily", label=_("Based on")) + rotate_after_day = forms.IntegerField(initial=5, label=_("Rotate after day")) start_date = forms.DateField( - initial=datetime.date.today, widget=forms.DateInput, label=_trans("Start date") + initial=datetime.date.today, widget=forms.DateInput, label=_("Start date") ) class Meta: @@ -783,9 +947,9 @@ class RotatingWorkTypeAssignForm(ModelForm): "is_active": HiddenInput(), } labels = { - "is_active": _trans("Is Active"), - "rotate_every_weekend": _trans("Rotate every weekend"), - "rotate_every": _trans("Rotate every"), + "is_active": _("Is Active"), + "rotate_every_weekend": _("Rotate every weekend"), + "rotate_every": _("Rotate every"), } def __init__(self, *args, **kwargs): @@ -795,60 +959,133 @@ class RotatingWorkTypeAssignForm(ModelForm): 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["start_date"].widget = forms.DateInput( - attrs={ - "class": "w-100 oh-input", - "type": "date", - "style": " height:50px; border-radius:0;", - } - ) - 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", - } - ) + 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["start_date"].widget = forms.DateInput( + attrs={ + "class": "w-100 oh-input", + "type": "date", + "style": " height:50px; border-radius:0;", + } + ) + 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", + } + ) + else: + super().__init__(*args, **kwargs) + + reload_queryset(self.fields) + request = getattr(horilla_middlewares._thread_locals, "request", None) + self.fields["employee_id"].initial = request.GET.get("emp_id") + + if not self.instance.pk and not request.GET.get("emp_id"): + self.fields["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=_("Employees"), + ) + 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["start_date"].widget = forms.DateInput( + attrs={ + "class": "w-100 oh-input", + "type": "date", + "style": " height:50px; border-radius:0;", + } + ) + 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] + if self.instance.pk: + return self.cleaned_data.get("employee_id") else: - return ValidationError(_("This field is required")) + employee_ids = self.cleaned_data.get("employee_id") + if isinstance(employee_ids, Employee): + return employee_ids + else: + if employee_ids: + return employee_ids[0] + else: + return ValidationError(_("This field is required")) def clean(self): + if self.instance.pk: + return super().clean() + super().clean() self.instance.employee_id = Employee.objects.filter( id=self.data.get("employee_id") @@ -864,38 +1101,24 @@ class RotatingWorkTypeAssignForm(ModelForm): 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() + if self.instance.pk: + employee = Employee.objects.get(id=self.instance.pk) 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 = RotatingWorkType.objects.get( + id=self.data["rotating_work_type_id"] ) - rotating_work_type_assign.rotate_every = self.cleaned_data["rotate_every"] - rotating_work_type_assign.rotate_every_weekend = self.cleaned_data[ - "rotate_every_weekend" + 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()) + 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 @@ -903,23 +1126,87 @@ class RotatingWorkTypeAssignForm(ModelForm): 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"] + start_date = self.instance.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 + self.instance.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"] + 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) - rotating_work_type_assign.next_change_date = next_date + self.instance.next_change_date = next_date elif based_on == "after": - rotating_work_type_assign.next_change_date = ( - rotating_work_type_assign.start_date + self.instance.next_change_date = ( + self.instance.start_date + datetime.timedelta(days=int(self.data.get("rotate_after_day"))) ) + return super().save() - rotating_work_type_assign.save() + else: + 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_type2 + rotating_work_type_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_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 + + datetime.timedelta( + days=int(self.data.get("rotate_after_day")) + ) + ) + + rotating_work_type_assign.save() class RotatingWorkTypeAssignUpdateForm(forms.ModelForm): @@ -927,9 +1214,7 @@ class RotatingWorkTypeAssignUpdateForm(forms.ModelForm): RotatingWorkTypeAssign model's form """ - based_on = forms.ChoiceField( - choices=BASED_ON, initial="daily", label=_trans("Based on") - ) + based_on = forms.ChoiceField(choices=BASED_ON, initial="daily", label=_("Based on")) class Meta: """ @@ -949,12 +1234,12 @@ class RotatingWorkTypeAssignUpdateForm(forms.ModelForm): "start_date": DateInput(attrs={"type": "date"}), } 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"), + "start_date": _("Start date"), + "rotate_after_day": _("Rotate after day"), + "rotate_every_weekend": _("Rotate every weekend"), + "rotate_every": _("Rotate every"), + "based_on": _("Based on"), + "is_active": _("Is Active"), } def __init__(self, *args, **kwargs): @@ -1047,6 +1332,8 @@ class EmployeeTypeForm(ModelForm): EmployeeType form """ + cols = {"employee_type": 12, "company_id": 12} + class Meta: """ Meta class for additional options @@ -1062,6 +1349,14 @@ class EmployeeShiftForm(ModelForm): EmployeeShift Form """ + cols = { + "employee_shift": 12, + "weekly_full_time": 12, + "full_time": 12, + "company_id": 12, + "grace_time_id": 12, + } + class Meta: """ Meta class for additional options @@ -1168,18 +1463,37 @@ class EmployeeShiftScheduleForm(ModelForm): EmployeeShiftSchedule model's form """ + cols = {"day": 12, "company_id": 12} + day = forms.ModelMultipleChoiceField( queryset=EmployeeShiftDay.objects.all(), ) + # day = forms.ModelMultipleChoiceField( + # queryset=EmployeeShiftDay.objects.all(), + # widget=forms.SelectMultiple(attrs={ + # "class": "oh-select oh-select2 w-100", + # "style": "height:45px;" + # }) + # ) + class Meta: """ Meta class for additional options """ model = EmployeeShiftSchedule - fields = "__all__" - exclude = ["is_night_shift", "is_active"] + fields = [ + "shift_id", + "minimum_working_hour", + "start_time", + "end_time", + "is_auto_punch_out_enabled", + "auto_punch_out_time", + "company_id", + "day", + ] + exclude = ["is_active", "day", "is_night_shift"] widgets = { "start_time": DateInput(attrs={"type": "time"}), "end_time": DateInput(attrs={"type": "time"}), @@ -1191,6 +1505,7 @@ class EmployeeShiftScheduleForm(ModelForm): # 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"), @@ -1203,6 +1518,17 @@ class EmployeeShiftScheduleForm(ModelForm): ) kwargs["initial"] = initial super().__init__(*args, **kwargs) + + if self.instance.pk: + self.fields["day"] = forms.ModelChoiceField( + queryset=EmployeeShiftDay.objects.all(), + widget=forms.Select( + attrs={ + "class": "oh-select oh-select2 w-100", + "style": "height:45px;", + } + ), + ) 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"): @@ -1249,41 +1575,57 @@ class EmployeeShiftScheduleForm(ModelForm): ) } ) + if self.instance.pk: + shift_id = cleaned_data.get("shift_id") + day_field = self["day"].value() + if day_field and not hasattr(day_field, "__iter__"): + day_field = [day_field] + + if self.instance.pk and shift_id and day_field: + shift = EmployeeShiftSchedule.objects.filter( + day=day_field, shift_id=shift_id + ) + shifts = shift.first() + if shift.exclude(pk=self.instance.pk).exists(): + raise ValidationError( + _( + f"Shift schedule already exists for '{shifts.day}' on '{shift_id}' " + ) + ) 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): + if not self.instance.pk: + 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 not self.instance.pk: + 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() + if days.first() is None: + raise ValidationError(_("Employee not chosen")) + return days.first() class RotatingShiftForm(ModelForm): @@ -1291,6 +1633,8 @@ class RotatingShiftForm(ModelForm): RotatingShift model's form """ + cols = {"name": 12, "shift1": 12, "shift2": 12} + class Meta: """ Meta class for additional options @@ -1309,12 +1653,13 @@ class RotatingShiftForm(ModelForm): def create_shift_field(shift_key, required, initial=None): self.fields[shift_key] = forms.ModelChoiceField( queryset=EmployeeShift.objects.all(), - widget=forms.Select( + widget=CustomModelChoiceWidget( + delete_url="/add-remove-shift-fields", attrs={ "class": "oh-select oh-select-2 mb-3", "name": shift_key, "id": f"id_{shift_key}", - } + }, ), required=required, empty_label=_("---Choose Shift---"), @@ -1393,22 +1738,30 @@ class RotatingShiftAssignForm(forms.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")) + cols = { + "employee_id": 12, + "rotating_shift_id": 12, + "start_date": 12, + "based_on": 12, + "rotate_after_day": 12, + "rotate_every_weekend": 12, + "rotate_every": 12, + } + + # 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=_("Employees"), + # ) + based_on = forms.ChoiceField(choices=BASED_ON, initial="daily", label=_("Based on")) + rotate_after_day = forms.IntegerField(initial=5, label=_("Rotate after day")) start_date = forms.DateField( - initial=datetime.date.today, widget=forms.DateInput, label=_trans("Start date") + initial=datetime.date.today, widget=forms.DateInput, label=_("Start date") ) class Meta: @@ -1429,77 +1782,149 @@ class RotatingShiftAssignForm(forms.ModelForm): "start_date": DateInput(attrs={"type": "date"}), } labels = { - "rotating_shift_id": _trans("Rotating Shift"), + "rotating_shift_id": _("Rotating Shift"), "start_date": _("Start date"), - "is_active": _trans("Is Active"), - "rotate_every_weekend": _trans("Rotate every weekend"), - "rotate_every": _trans("Rotate every"), + "is_active": _("Is Active"), + "rotate_every_weekend": _("Rotate every weekend"), + "rotate_every": _("Rotate every"), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) reload_queryset(self.fields) + request = getattr(_thread_locals, "request", None) 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 \ + 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["start_date"].widget = forms.DateInput( - attrs={ - "class": "w-100 oh-input", - "type": "date", - "style": " height:50px; border-radius:0;", - } - ) - 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", - } - ) + "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["start_date"].widget = forms.DateInput( + attrs={ + "class": "w-100 oh-input", + "type": "date", + "style": " height:50px; border-radius:0;", + } + ) + 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", + } + ) + else: + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + self.fields["employee_id"].initial = request.GET.get("emp_id") + if not self.instance.pk and not request.GET.get("emp_id"): + self.fields["employee_id"] = HorillaMultiSelectField( + queryset=Employee.objects.filter( + employee_work_info__isnull=False, is_active=True + ), + widget=HorillaMultiSelectWidget( + filter_route_name="employee-widget-filter", + filter_class=EmployeeFilter, + filter_instance_contex_name="f", + filter_template_path="employee_filters.html", + ), + label=_("Employees"), + ) + + 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["start_date"].widget = forms.DateInput( + attrs={ + "class": "w-100 oh-input", + "type": "date", + "style": " height:50px; border-radius:0;", + } + ) + 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] + if self.instance.pk: + return self.cleaned_data.get("employee_id") else: - return ValidationError(_("This field is required")) + employee_ids = self.cleaned_data.get("employee_id") + if isinstance(employee_ids, Employee): + return employee_ids + else: + if employee_ids: + return employee_ids[0] + else: + return ValidationError(_("This field is required")) def clean(self): + if self.instance.pk: + return super().clean() super().clean() self.instance.employee_id = Employee.objects.filter( id=self.data.get("employee_id") @@ -1514,58 +1939,92 @@ class RotatingShiftAssignForm(forms.ModelForm): 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" + def save(self, commit=False): + if self.instance.pk: + day_name = self.cleaned_data["rotate_every_weekend"] + day_names = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", ] - 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 + target_day = day_names.index(day_name.lower()) + based_on = self.cleaned_data["based_on"] - start_date = self.cleaned_data["start_date"] + start_date = self.instance.start_date if based_on == "weekly": next_date = get_next_week_date(target_day, start_date) - rotating_shift_assign.next_change_date = next_date + self.instance.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"] + 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) - rotating_shift_assign.next_change_date = next_date + self.instance.next_change_date = next_date elif based_on == "after": - rotating_shift_assign.next_change_date = ( - rotating_shift_assign.start_date + self.instance.next_change_date = ( + self.instance.start_date + datetime.timedelta(days=int(self.data.get("rotate_after_day"))) ) - rotating_shift_assign.save() + return super().save() + else: + 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 + + datetime.timedelta( + days=int(self.data.get("rotate_after_day")) + ) + ) + rotating_shift_assign.save() class RotatingShiftAssignUpdateForm(ModelForm): @@ -1573,9 +2032,7 @@ class RotatingShiftAssignUpdateForm(ModelForm): RotatingShiftAssign model's form """ - based_on = forms.ChoiceField( - choices=BASED_ON, initial="daily", label=_trans("Based on") - ) + based_on = forms.ChoiceField(choices=BASED_ON, initial="daily", label=_("Based on")) class Meta: """ @@ -1595,12 +2052,12 @@ class RotatingShiftAssignUpdateForm(ModelForm): "start_date": DateInput(attrs={"type": "date"}), } 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"), + "start_date": _("Start date"), + "rotate_after_day": _("Rotate after day"), + "rotate_every_weekend": _("Rotate every weekend"), + "rotate_every": _("Rotate every"), + "based_on": _("Based on"), + "is_active": _("Is Active"), } def __init__(self, *args, **kwargs): @@ -1692,6 +2149,8 @@ class ShiftRequestForm(ModelForm): ShiftRequest model's form """ + cols = {"description": 12} + class Meta: """ Meta class for additional options @@ -1714,9 +2173,9 @@ class ShiftRequestForm(ModelForm): "requested_till": DateInput(attrs={"type": "date"}), } labels = { - "description": _trans("Description"), - "requested_date": _trans("Requested Date"), - "requested_till": _trans("Requested Till"), + "description": _("Description"), + "requested_date": _("Requested Date"), + "requested_till": _("Requested Till"), } def as_p(self): @@ -1744,6 +2203,8 @@ class ShiftAllocationForm(ModelForm): ShiftRequest model's form """ + cols = {"description": 12} + class Meta: """ Meta class for additional options @@ -1767,9 +2228,9 @@ class ShiftAllocationForm(ModelForm): } labels = { - "description": _trans("Description"), - "requested_date": _trans("Requested Date"), - "requested_till": _trans("Requested Till"), + "description": _("Description"), + "requested_date": _("Requested Date"), + "requested_till": _("Requested Till"), } def __init__(self, *args, **kwargs): @@ -1778,6 +2239,7 @@ class ShiftAllocationForm(ModelForm): { "hx-target": "#id_reallocate_to_parent_div", "hx-trigger": "change", + "hx-swap": "innerHTML", "hx-get": "/update-employee-allocation", } ) @@ -1807,6 +2269,8 @@ class WorkTypeRequestForm(ModelForm): WorkTypeRequest model's form """ + cols = {"description": 12} + class Meta: """ Meta class for additional options @@ -1826,9 +2290,9 @@ class WorkTypeRequestForm(ModelForm): "requested_till": DateInput(attrs={"type": "date"}), } labels = { - "requested_date": _trans("Requested Date"), - "requested_till": _trans("Requested Till"), - "description": _trans("Description"), + "requested_date": _("Requested Date"), + "requested_till": _("Requested Till"), + "description": _("Description"), } def as_p(self): @@ -2142,11 +2606,31 @@ class TagsForm(ModelForm): return table_html +class EmployeeTagForm(ModelForm): + """ + Employee Tags form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = EmployeeTag + fields = "__all__" + exclude = ["is_active"] + widgets = {"color": TextInput(attrs={"type": "color", "style": "height:50px"})} + + class AuditTagForm(ModelForm): """ Audit Tags form """ + cols = { + "title": 12, + } + class Meta: """ Meta class for additional options @@ -2219,6 +2703,8 @@ class MailTemplateForm(ModelForm): MailTemplateForm """ + cols = {"title": 12, "body": 12, "company_id": 12} + class Meta: model = HorillaMailTemplate fields = "__all__" @@ -2226,6 +2712,7 @@ class MailTemplateForm(ModelForm): "body": forms.Textarea( attrs={"data-summernote": "", "style": "display:none;"} ), + "is_active": forms.HiddenInput(), } def get_template_language(self): @@ -2274,6 +2761,11 @@ class MailTemplateForm(ModelForm): class MultipleApproveConditionForm(ModelForm): + + cols = { + "multi_approval_manager": 12, + } + CONDITION_CHOICE = [ ("equal", _("Equal (==)")), ("notequal", _("Not Equal (!=)")), @@ -2366,6 +2858,16 @@ class AnnouncementForm(ModelForm): label="Employees", ) + cols = { + "title": 12, + "description": 12, + "attachments": 12, + "expire_date": 12, + "employees": 12, + "department": 12, + "job_position": 12, + } + class Meta: """ Meta class for additional options @@ -2681,6 +3183,8 @@ class HolidayForm(ModelForm): - end_date: A DateField representing the end date of the holiday. """ + cols = {"name": 12} + start_date = forms.DateField( widget=forms.DateInput(attrs={"type": "date"}), ) @@ -2708,7 +3212,7 @@ class HolidayForm(ModelForm): fields = "__all__" exclude = ["is_active"] labels = { - "name": _("Name"), + "name": _("Holiday Name"), } def __init__(self, *args, **kwargs): @@ -2769,6 +3273,8 @@ class CompanyLeaveForm(ModelForm): - exclude: A list of fields to exclude from the form (is_active). """ + cols = {"based_on_week": 12, "based_on_week_day": 12} + class Meta: """ Meta class for additional options diff --git a/base/horilla_company_manager.py b/base/horilla_company_manager.py index c2ff4e982..e3a541e8a 100644 --- a/base/horilla_company_manager.py +++ b/base/horilla_company_manager.py @@ -9,13 +9,21 @@ 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 +from horilla.signals import ( + post_bulk_update, + post_model_clean, + pre_bulk_update, + pre_model_clean, +) logger = logging.getLogger(__name__) django_filter_update = QuerySet.update def update(self, *args, **kwargs): + """ + Bulk Update + """ # pre_update signal request = getattr(_thread_locals, "request", None) self.request = request @@ -27,6 +35,21 @@ def update(self, *args, **kwargs): return result +django_model_clean = models.Model.clean + + +def clean(self, *args, **kwargs): + """ + Method to override django clean and trigger to signals + """ + pre_model_clean.send(sender=self._meta.model, instance=self, **kwargs) + result = django_model_clean(self) + post_model_clean.send(sender=self._meta.model, instance=self, **kwargs) + return result + + +models.Model.clean = clean + setattr(QuerySet, "update", update) @@ -83,6 +106,8 @@ class HorillaCompanyManager(models.Manager): request = getattr(_thread_locals, "request", None) if not getattr(request, "is_filtering", None): queryset = queryset.filter(is_active=True) + elif model_name == "offboardingemployee": + return queryset else: for field in queryset.model._meta.fields: if isinstance(field, models.ForeignKey): diff --git a/base/methods.py b/base/methods.py index aa8b8f188..8c3aca929 100644 --- a/base/methods.py +++ b/base/methods.py @@ -637,6 +637,7 @@ def reload_queryset(fields): def check_manager(employee, instance): + try: if isinstance(instance, Employee): return instance.employee_work_info.reporting_manager_id == employee diff --git a/base/models.py b/base/models.py index 4b925cb3c..4281f8385 100644 --- a/base/models.py +++ b/base/models.py @@ -1,6 +1,4 @@ """ -models.py - This module is used to register django models """ @@ -10,10 +8,17 @@ from typing import Iterable import django from django.apps import apps +from django.conf import settings from django.contrib import messages from django.contrib.auth.models import AbstractUser, User from django.core.exceptions import ValidationError +from django.core.files.storage import default_storage from django.db import models +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver +from django.urls import reverse, reverse_lazy +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager @@ -21,6 +26,7 @@ from horilla import horilla_middlewares from horilla.horilla_middlewares import _thread_locals from horilla.models import HorillaModel from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog +from horilla_views.cbv_methods import render_template # Create your models here. WEEKS = [ @@ -60,6 +66,9 @@ def validate_time_format(value): def clear_messages(request): + """ + clear messages + """ storage = messages.get_messages(request) for message in storage: pass @@ -98,6 +107,36 @@ class Company(HorillaModel): def __str__(self) -> str: return str(self.company) + def company_icon_with_name(self): + from django.utils.html import format_html + + return format_html( + ' {}', + self.icon.url, + self.company, + ) + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("company-update-form", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_delete_instance(self): + """ + to get instance for delete + """ + + return self.pk + class Department(HorillaModel): """ @@ -112,6 +151,10 @@ class Department(HorillaModel): objects = HorillaCompanyManager() class Meta: + """ + meta + """ + verbose_name = _("Department") verbose_name_plural = _("Departments") @@ -131,6 +174,50 @@ class Department(HorillaModel): raise ValidationError("This department already exists in this company") return + def toggle_count(self): + return self.job_position.all().count() + + def get_department_col(self): + """ + this method is to get custom department col in job position + """ + + return render_template( + path="cbv/settings/job_position_dpt.html", + context={"instance": self}, + ) + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("settings-department-update", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_delete_instance(self): + """ + to get instance for delete + """ + + return self.pk + + def get_job_position_col(self): + """ + this method is to get custom job position col in job position + """ + + return render_template( + path="cbv/settings/position_in_job_position.html", + context={"instance": self}, + ) + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) @@ -169,6 +256,29 @@ class JobPosition(HorillaModel): def __str__(self): return str(self.job_position + " - (" + self.department_id.department) + ")" + def job_position_col(self): + """ + This method for get custom column . + """ + + return render_template( + path="cbv/settings/job_position_col_in_job_role.html", + context={"instance": self}, + ) + + def get_data_count(self): + return self.jobrole_set.all().count() + + def job_role_col(self): + """ + This method for get custom column . + """ + + return render_template( + path="cbv/settings/job_role.html", + context={"instance": self}, + ) + class JobRole(HorillaModel): """JobRole model""" @@ -231,6 +341,27 @@ class WorkType(HorillaModel): raise ValidationError("This work type already exists in this company") return + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("work-type-update-form", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_delete_instance(self): + """ + to get instance for delete + """ + + return self.pk + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) @@ -278,6 +409,38 @@ class RotatingWorkType(HorillaModel): def __str__(self) -> str: return str(self.name) + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("rotating-work-type-update-form", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_delete_instance(self): + """ + to get instance for delete + """ + + return self.pk + + def get_additional_worktytpes(self): + """ + this method is to get additional work types if exists + """ + + additional_work = self.additional_work_types() + if additional_work: + additional = "
    ".join([str(work) for work in additional_work]) + return additional + return "None" + def clean(self): if self.work_type1 == self.work_type2: raise ValidationError(_("Select different work type continuously")) @@ -418,6 +581,9 @@ class RotatingWorkTypeAssign(HorillaModel): ordering = ["-next_change_date", "-employee_id__employee_first_name"] def clean(self): + if self.start_date < django.utils.timezone.now().date(): + raise ValidationError(_("Date must be greater than or equal to today")) + 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( @@ -425,8 +591,72 @@ class RotatingWorkTypeAssign(HorillaModel): ) 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")) + + def rotate_data(self): + """ + method for rotate col + """ + + return render_template( + path="cbv/rotating_work_type/rotation_col.html", + context={"instance": self}, + ) + + def get_based_on_display(self): + """ + Display work type + """ + return dict(BASED_ON).get(self.based_on) + + def get_actions(self): + """ + get different actions + """ + + return render_template( + path="cbv/rotating_work_type/work_rotate_actions.html", + context={"instance": self}, + ) + + def work_rotate_detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.employee_id.employee_work_info.department_id} / {self.employee_id.employee_work_info.job_position_id}" + + def work_rotate_detail_view(self): + """ + for detail view of page + """ + url = reverse("work-rotating-detail-view", kwargs={"pk": self.pk}) + return url + + def individual_tab_work_rotate_detail_view(self): + """ + for detail view of page in employee profile + """ + url = reverse("individual-work-rotating-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_is_active(self): + """ + return active or not + """ + + if self.is_active: + return "Is Active" + else: + return "Archived" + + def get_detail_view_actions(self): + """ + get detail view actions + """ + + return render_template( + path="cbv/rotating_work_type/rotate_detail_view_actions.html", + context={"instance": self}, + ) class EmployeeType(HorillaModel): @@ -450,6 +680,23 @@ class EmployeeType(HorillaModel): def __str__(self) -> str: return str(self.employee_type) + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("employee-type-update-view", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_instance_id(self): + return self.id + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) @@ -541,6 +788,29 @@ class EmployeeShift(HorillaModel): def __str__(self) -> str: return str(self.employee_shift) + def get_grace_time(self): + if self.grace_time_id: + return self.grace_time_id + else: + return "Nil" + + def get_instance_id(self): + return self.id + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("employee-shift-update-view", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) @@ -574,10 +844,7 @@ class EmployeeShiftSchedule(HorillaModel): """ day = models.ForeignKey( - EmployeeShiftDay, - on_delete=models.PROTECT, - related_name="day_schedule", - verbose_name=_("Shift Day"), + EmployeeShiftDay, on_delete=models.PROTECT, related_name="day_schedule" ) shift_id = models.ForeignKey( EmployeeShift, on_delete=models.PROTECT, verbose_name=_("Shift") @@ -604,6 +871,7 @@ class EmployeeShiftSchedule(HorillaModel): "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") @@ -632,11 +900,66 @@ class EmployeeShiftSchedule(HorillaModel): def __str__(self) -> str: return f"{self.shift_id.employee_shift} {self.day}" + def get_detail_url(self): + """ + Detail view url + """ + url = reverse_lazy("employee-shift-shedule-detail-view", kwargs={"pk": self.pk}) + return url + + def get_instance_id(self): + return self.id + + def get_automatic_check_out_time(self): + """ + Custome column for automatic checkout time + """ + return ( + f"
    Automatic Check Out Time
    {self.auto_punch_out_time}
    " + if self.is_auto_punch_out_enabled + else "" + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.day.day}&background=random" + return url + + def actions_col(self): + """ + This for actions column in employee shift schedule + """ + return render_template( + path="cbv/settings/employee_shift_schedule_action.html", + context={"instance": self}, + ) + + def detail_actions_col(self): + """ + This for detail actions column in employee shift schedule + """ + return render_template( + path="cbv/settings/employee_shift_schedule_detail_action.html", + context={"instance": self}, + ) + + def auto_punch_out_col(self): + return "Yes" if self.is_auto_punch_out_enabled else "No" + 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) + def day_col(self): + """ + Custom column for day in employee shift schedule + """ + + return dict(DAY).get(self.day.day) + class RotatingShift(HorillaModel): """ @@ -681,6 +1004,33 @@ class RotatingShift(HorillaModel): def __str__(self) -> str: return str(self.name) + def get_additional_shifts(self): + """ + Returns a list of additional shifts or a message if no additional shifts are available. + """ + additional_shifts = self.additional_shifts() + if additional_shifts: + additional_shift = "
    ".join([str(shift) for shift in additional_shifts]) + return additional_shift + return "None" + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("rotating-shift-update", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_instance_id(self): + return self.id + def clean(self): additional_shifts = ( @@ -804,6 +1154,96 @@ class RotatingShiftAssign(HorillaModel): ) objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") + def rotating_column(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/rotating_shift/rotating_column.html", + context={"instance": self}, + ) + + def actions(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/rotating_shift/actions_rotaing_shift.html", + context={"instance": self}, + ) + + def rotating_detail_actions(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/rotating_shift/rotating_shift_detail_actions.html", + context={"instance": self}, + ) + + def get_based_on_display(self): + """ + Display work type + """ + return dict(BASED_ON).get(self.based_on) + + def rotating_shift_detail(self): + """ + detail view + """ + + url = reverse("rotating-shift-detail-view", kwargs={"pk": self.pk}) + + return url + + def rotating_shift_individual_detail(self): + """ + individual detail view + """ + + url = reverse("rotating-shift-individual-detail-view", kwargs={"pk": self.pk}) + + return url + + def rotating_subtitle(self): + """ + Detail view subtitle + """ + + return f"""{self.employee_id.employee_work_info.department_id } / + { self.employee_id.employee_work_info.job_position_id}""" + + def check_active(self): + """ + Check active + """ + + if self.is_active: + return "Is Active" + else: + return "Archived" + + def detail_edit_url(self): + """ + Detail view edit + """ + + url = reverse("rotating-shift-assign-update", kwargs={"id": self.pk}) + + return url + + def detail_archive_url(self): + """ + Detail view edit + """ + + url = reverse("rotating-shift-assign-archive", kwargs={"obj_id": self.pk}) + + return url + class Meta: """ Meta class to add additional options @@ -821,6 +1261,7 @@ class RotatingShiftAssign(HorillaModel): ) 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")) @@ -892,6 +1333,66 @@ class WorkTypeRequest(HorillaModel): "-id", ] + def comment_note(self): + """ + method used for comment note col in the page + """ + + return render_template( + path="cbv/work_type_request/note.html", + context={"instance": self}, + ) + + def work_actions(self): + """ + method for rendering actions(edit,duplicate,delete) + """ + + return render_template( + path="cbv/work_type_request/actions.html", + context={"instance": self}, + ) + + def confirmation(self): + """ + method for rendering options(approve,reject) + """ + + return render_template( + path="cbv/work_type_request/confirmation.html", + context={"instance": self}, + ) + + def detail_view(self): + """ + for detail view of page + """ + url = reverse("work-detail-view", kwargs={"pk": self.pk}) + return url + + def is_permanent_work_type_display(self): + """ + Method to display "Yes" or "No" based on is_permanent_work_type value + """ + return "Yes" if self.is_permanent_work_type else "No" + + def detail_view_actions(self): + """ + method for rendering different options + convert,skillzone,reject,mail + """ + + return render_template( + path="cbv/work_type_request/detail_view_actions.html", + context={"instance": self}, + ) + + def detail_subtitle(self): + """ + Return subtitle containing both department and job position information. + """ + return f"{self.employee_id.employee_work_info.department_id} / {self.employee_id.employee_work_info.job_position_id}" + def delete(self, *args, **kwargs): request = getattr(_thread_locals, "request", None) if not self.approved: @@ -1058,6 +1559,125 @@ class ShiftRequest(HorillaModel): "-id", ] + def comment(self): + """ + This method for get custome coloumn for comment. + """ + + return render_template( + path="cbv/shift_request/comment.html", + context={"instance": self}, + ) + + # def shift_allocate_actions(self): + # """ + # This method for get custome coloumn for allocated actions. + # """ + + # return render_template( + # path="cbv/shift_request/allocated_shift_actions.html", + # context={"instance": self}, + # ) + + def allocated_confirm_action_col(self): + """ + This method for get custome coloumn for allocated actions. + """ + + return render_template( + path="cbv/shift_request/allocated_confirm_action.html", + context={"instance": self}, + ) + + def user_availability(self): + """ + This method for get custome coloumn for User availability. + """ + + return render_template( + path="cbv/shift_request/user_availability.html", + context={"instance": self}, + ) + + def shift_details(self): + """ + Detail view + """ + + url = reverse("shift-detail-view", kwargs={"pk": self.pk}) + + return url + + def allocate_shift_details(self): + """ + Allocate detail view + """ + + url = reverse("allocate-detail-view", kwargs={"pk": self.pk}) + + return url + + def is_permanent(self): + """ + Permanent shift + """ + return "Yes" if self.is_permanent_shift else "No" + + def shift_actions(self): + """ + This method for get custome coloumn for actions. + """ + + return render_template( + path="cbv/shift_request/actions_shift_requst.html", + context={"instance": self}, + ) + + def confirmations(self): + """ + This method for get custome coloumn for confirmations. + """ + + return render_template( + path="cbv/shift_request/confirmations.html", + context={"instance": self}, + ) + + def allocate_confirmations(self): + """ + This method for get custome coloumn for confirmations. + """ + + return render_template( + path="cbv/shift_request/confirm_allocated.html", + context={"instance": self}, + ) + + def detail_actions(self): + """ + This method for get custome coloumn for comment. + """ + + return render_template( + path="cbv/shift_request/shift_deatil_actions.html", + context={"instance": self}, + ) + + def request_status(self): + return ( + _("Rejected") + if self.canceled + else (_("Approved") if self.approved else _("Requested")) + ) + + def details_subtitle(self): + """ + Detail view subtitle + """ + + return f"""{self.employee_id.employee_work_info.department_id } / + { self.employee_id.employee_work_info.job_position_id}""" + def clean(self): request = getattr(horilla_middlewares._thread_locals, "request", None) @@ -1164,6 +1784,41 @@ class Tags(HorillaModel): def __str__(self): return self.title + def get_color(self): + """ + This method returns the style string with the tag's color + """ + color = ( + f"" + ) + return color + + def get_instance_id(self): + """ + To get instance + """ + return self.id + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("update-helpdesk-tag", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("tag-delete", kwargs={"obj_id": self.pk}) + # message = "Are you sure you want to delete this tag ?" + # return f"'{url}'" + "," + f"'{message}'" + return url + class HorillaMailTemplate(HorillaModel): title = models.CharField(max_length=100, unique=True) @@ -1235,6 +1890,20 @@ class DynamicEmailConfiguration(HorillaModel): Company, on_delete=models.CASCADE, null=True, blank=True ) + def highlight_cell(self): + if self.is_primary: + return f'style="background-color: rgba(255, 68, 0, 0.134);" ' + + def action_col(self): + """ + This method for get custome coloumn . + """ + + return render_template( + path="cbv/settings/mail_server_action.html", + context={"instance": self}, + ) + def clean(self): if self.use_ssl and self.use_tls: raise ValidationError( @@ -1286,6 +1955,10 @@ CONDITION_CHOICE = [ class MultipleApprovalCondition(HorillaModel): + """ + Multiple approve conditions + """ + department = models.ForeignKey(Department, on_delete=models.CASCADE) condition_field = models.CharField( max_length=255, @@ -1312,7 +1985,6 @@ class MultipleApprovalCondition(HorillaModel): blank=True, verbose_name=_("Ending Value"), ) - objects = models.Manager() company_id = models.ForeignKey( Company, null=True, @@ -1320,10 +1992,78 @@ class MultipleApprovalCondition(HorillaModel): on_delete=models.CASCADE, verbose_name=_("Company"), ) + objects = HorillaCompanyManager() def __str__(self) -> str: return f"{self.condition_field} {self.condition_operator}" + def get_condition_field(self): + """ + Display condition field + """ + return dict(FIELD_CHOICE).get(self.condition_field) + + def get_condition_operator(self): + """ + Display condition operator + """ + return dict(CONDITION_CHOICE).get(self.condition_operator) + + def get_condition_value(self): + """ + Condition value column + """ + if self.condition_operator == "range": + start_value = self.condition_start_value + end_value = self.condition_end_value + return start_value + " - " + end_value + else: + return self.condition_value + + def approval_managers_col(self): + """ + For approval managers column + """ + + return render_template( + path="cbv/multiple_approval_condition/approval_managers.html", + context={"instance": self}, + ) + + def detail_actions(self): + """ + For detail action column + """ + + return render_template( + path="cbv/multiple_approval_condition/detail_action.html", + context={"instance": self}, + ) + + def actions_col(self): + """ + For actions column + """ + + return render_template( + path="cbv/multiple_approval_condition/actions.html", + context={"instance": self}, + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.department}&background=random" + return url + + def detail_view(self): + """ + detail view + """ + url = reverse("detail-view-multiple-approval-condition", kwargs={"pk": self.pk}) + return url + def clean(self, *args, **kwargs): if self.condition_value: instance = MultipleApprovalCondition.objects.filter( @@ -1414,6 +2154,9 @@ class MultipleApprovalCondition(HorillaModel): super().save(*args, **kwargs) def approval_managers(self, *args, **kwargs): + """ + approved managers + """ managers = [] from employee.models import Employee @@ -1433,13 +2176,17 @@ class MultipleApprovalCondition(HorillaModel): class MultipleApprovalManagers(models.Model): + """ + Multiple approve + """ + 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() + objects = HorillaCompanyManager(related_company_field="condition_id__company_id") class Meta: verbose_name = _("Multiple Approval Managers") @@ -1551,6 +2298,9 @@ class Announcement(HorillaModel): return self.announcementview_set.filter(viewed=True) def viewed_by(self): + """ + Announcement view + """ viewed_by = AnnouncementView.objects.filter( announcement_id__id=self.id, viewed=True @@ -1571,6 +2321,18 @@ class Announcement(HorillaModel): def __str__(self): return self.title + def announcement_custom_col(self): + """ + custom col for announcement list col + """ + + current_date = datetime.now().strftime("%Y-%m-%d") + + return render_template( + path="cbv/dashboard/announcement_title.html", + context={"instance": self, "current_date": current_date}, + ) + class AnnouncementComment(HorillaModel): """ @@ -1594,8 +2356,21 @@ class AnnouncementView(models.Model): 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() + def announcement_viewed_by_col(self): + """ + custom col for announcement list col + """ + + return render_template( + path="cbv/dashboard/announcement_viewed_by.html", + context={ + "instance": self, + }, + ) + class EmailLog(models.Model): """ @@ -1614,6 +2389,33 @@ class EmailLog(models.Model): Company, on_delete=models.CASCADE, null=True, editable=False ) + def __str__(self) -> str: + return f"{self.subject} {self.to}" + + def status_display(self): + status = dict(self.statuses).get(self.status) + if self.status == "sent": + color_class = "oh-dot--success" + link_class = "link-success" + + elif self.status == "failed": + color_class = "oh-dot--danger" + link_class = "link-danger" + return format_html( + '' + '{status}', + color_class=color_class, + status=status, + link_class=link_class, + ) + + def mail_log_detail_view(self): + """ + for detail view of page + """ + url = reverse("individual-mail-log-detail", kwargs={"pk": self.pk}) + return url + class DriverViewed(models.Model): """ @@ -1636,6 +2438,10 @@ class DriverViewed(models.Model): class DashboardEmployeeCharts(HorillaModel): + """ + dashboard employee chart + """ + from employee.models import Employee employee = models.ForeignKey(Employee, on_delete=models.CASCADE) @@ -1652,6 +2458,10 @@ class DashboardEmployeeCharts(HorillaModel): class BiometricAttendance(models.Model): + """ + Biometric attendance + """ + is_installed = models.BooleanField(default=False) company_id = models.ForeignKey( Company, @@ -1743,6 +2553,46 @@ class Holidays(HorillaModel): def __str__(self): return self.name + def detail_view(self): + """ + detail view + """ + + url = reverse("holiday-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_view_actions(self): + """ + detail view actions + """ + return render_template( + path="cbv/holidays/detail_view_actions.html", + context={"instance": self}, + ) + + def get_recurring_status(self): + """ + recurring data + """ + return "Yes" if self.recurring else "No" + + def holidays_actions(self): + """ + method for rendering actions(edit,delete) + """ + + return render_template( + path="cbv/holidays/holidays_actions.html", + context={"instance": self}, + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.name}&background=random" + return url + def today_holidays(today=None) -> models.QuerySet: """ Retrieve holidays that overlap with the given date (default is today). @@ -1782,6 +2632,72 @@ class CompanyLeaves(HorillaModel): def __str__(self): return f"{dict(WEEK_DAYS).get(self.based_on_week_day)} | {dict(WEEKS).get(self.based_on_week)}" + def custom_based_on_week(self): + """ + custom based on col + """ + + return render_template( + path="cbv/company_leaves/on_week.html", + context={"instance": self, "weeks": WEEKS}, + ) + + def get_detail_title(self): + """ + for return title + """ + + title = "Company Leaves" + return title + + def detail_view_actions(self): + """ + detail view actions + """ + return render_template( + path="cbv/company_leaves/detail_view_actions.html", + context={"instance": self}, + ) + + def based_on_week_day_col(self): + """ + custom based on week day col + """ + + return render_template( + path="cbv/company_leaves/on_week_day.html", + context={"instance": self, "week_days": WEEK_DAYS}, + ) + + def company_leave_actions(self): + """ + custom actions col + """ + + return render_template( + path="cbv/company_leaves/company_leave_actions.html", + context={"instance": self, "weeks": WEEKS}, + ) + + def detail_view(self): + """ + detail view + """ + + url = reverse("company-leave-detail-view", kwargs={"pk": self.pk}) + return url + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + if self.based_on_week is not None: + url = f"https://ui-avatars.com/api/?name={dict(WEEKS).get(self.based_on_week)}&background=random" + else: + data = "All" + url = f"https://ui-avatars.com/api/?name={data}&background=random" + return url + class PenaltyAccounts(HorillaModel): """ @@ -1816,8 +2732,33 @@ class PenaltyAccounts(HorillaModel): ) minus_leaves = models.FloatField(default=0.0, null=True) deduct_from_carry_forward = models.BooleanField(default=False) + + def get_deduct_from_carry_forward(self): + if self.deduct_from_carry_forward: + return "Yes" + return "No" + penalty_amount = models.FloatField(default=0.0, null=True) + def get_delete_url(self): + """ + To get delete url + """ + url = reverse("delete-penalties", kwargs={"penalty_id": self.pk}) + return url + + def get_delete_instance(self): + """ + To get instance for delete + """ + return self.pk + + def penalty_type_col(self): + if apps.is_installed("attendance"): + if self.late_early_id: + return "Late come or Early out Penalty" + return "Leave Penalty" + def clean(self) -> None: super().clean() if apps.is_installed("leave") and not self.leave_type_id and self.minus_leaves: @@ -1832,7 +2773,11 @@ class PenaltyAccounts(HorillaModel): ) } ) - if not self.minus_leaves and not self.penalty_amount: + if ( + apps.is_installed("leave") + and not self.minus_leaves + and not self.penalty_amount + ): raise ValidationError( { "leave_type_id": _( @@ -1842,8 +2787,10 @@ class PenaltyAccounts(HorillaModel): ) if ( - self.minus_leaves or self.deduct_from_carry_forward - ) and not self.leave_type_id: + apps.is_installed("leave") + and (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 @@ -1862,4 +2809,134 @@ class NotificationSound(models.Model): sound_enabled = models.BooleanField(default=False) +@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 availabl 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_delete, sender=PenaltyAccounts) +# def delete_deduction_cutleave_from_penalty(sender, instance, **kwargs): +# """ +# This is a post delete method, used to delete deduction and update available leave days.""" +# # Check if the deduction model is installed +# if apps.is_installed("payroll"): +# Deduction = get_horilla_model_class(app_label="payroll", model="deduction") +# # Assuming deductions are related to PenaltyAccounts by a foreign key or similar +# deductions = Deduction.objects.filter(specific_employees=instance.employee_id, amount=instance.penalty_amount) + +# for deduction in deductions: +# deduction.delete() + +# 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() +# if available: +# unit = round(instance.minus_leaves * 2) / 2 +# if not instance.deduct_from_carry_forward: +# available.available_days += unit # Restore the deducted days +# else: +# available.carryforward_days += unit # Restore the deducted carryforward days + +# available.save() + + +@receiver(post_delete, sender=PenaltyAccounts) +def delete_deduction_cutleave_from_penalty(sender, instance, **kwargs): + """ + This is a post delete method, used to delete the deduction and update available leave days. + """ + # Check if the deduction model is installed + if apps.is_installed("payroll"): + Deduction = get_horilla_model_class(app_label="payroll", model="deduction") + + if instance.late_early_id: + title = f"{instance.late_early_id.get_type_display()} penalty" + elif instance.leave_request_id: + title = f"Leave penalty {instance.leave_request_id.end_date}" + else: + title = f"Penalty on {datetime.today()}" + + # Attempt to retrieve the deduction specifically associated with the penalty account + deductions = Deduction.objects.filter( + specific_employees=instance.employee_id, + amount=instance.penalty_amount, + title=title, + ) + + # If you have a date or other unique field, add it to the filter + if instance.late_early_id: + deductions = deductions.filter( + one_time_date=instance.late_early_id.attendance_id.attendance_date + ) + elif instance.leave_request_id: + deductions = deductions.filter( + one_time_date=instance.leave_request_id.end_date + ) + else: + deductions = deductions.filter(one_time_date=datetime.today()) + + for deduction in deductions: + deduction.delete() + + 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() + if available: + unit = round(instance.minus_leaves * 2) / 2 + if not instance.deduct_from_carry_forward: + available.available_days += unit # Restore the deducted days + else: + available.carryforward_days += ( + unit # Restore the deducted carryforward days + ) + + available.save() + + User.add_to_class("is_new_employee", models.BooleanField(default=False)) diff --git a/base/shift_decorators.py b/base/shift_decorators.py new file mode 100644 index 000000000..06e5a37df --- /dev/null +++ b/base/shift_decorators.py @@ -0,0 +1,51 @@ +""" +decorator functions for base +""" +from django.http import HttpResponse +from django.shortcuts import render +from base.models import MultipleApprovalManagers +from employee.models import EmployeeWorkInformation +from django.contrib import messages +decorator_with_arguments = ( + lambda decorator: lambda *args, **kwargs: lambda func: decorator( + func, *args, **kwargs + ) +) + +@decorator_with_arguments +def report_manager_can_enter(function, perm): + """ + This method is used to check permission to employee for enter to the function if the employee + do not have permission also checks, has reporting manager. + """ + + def _function(request, *args, **kwargs): + leave_perm = [ + "leave.view_leaverequest", + "leave.change_leaverequest", + "leave.delete_leaverequest", + ] + user = request.user + employee = user.employee_get + if perm in leave_perm: + is_approval_manager = MultipleApprovalManagers.objects.filter( + employee_id=employee.id + ).exists() + if is_approval_manager: + return function(request, *args, **kwargs) + is_manager = EmployeeWorkInformation.objects.filter( + reporting_manager_id=employee + ).exists() + if user.has_perm(perm) or is_manager: + return function(request, *args, **kwargs) + else: + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function + diff --git a/base/static/base/actions.js b/base/static/base/actions.js index e3ed4b34a..7d9408de5 100644 --- a/base/static/base/actions.js +++ b/base/static/base/actions.js @@ -1,3 +1,5 @@ + + var excelMessages = { ar: "هل ترغب في تنزيل ملف Excel؟", de: "Möchten Sie die Excel-Datei herunterladen?", diff --git a/base/static/cbv/deleteFunc.js b/base/static/cbv/deleteFunc.js new file mode 100644 index 000000000..940dd66c2 --- /dev/null +++ b/base/static/cbv/deleteFunc.js @@ -0,0 +1,26 @@ +function getCSRFToken() { + return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + } +function deleteItem(url,message) { + Swal.fire({ + text: message, + icon: "question", + showCancelButton: true, + confirmButtonColor: "green", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm" + }).then((result) => { + if (result.isConfirmed) { + const form = document.createElement('form'); + form.setAttribute('action', url); + form.setAttribute('method', 'post'); + const csrfTokenInput = document.createElement('input'); + csrfTokenInput.setAttribute('type', 'hidden'); + csrfTokenInput.setAttribute('name', 'csrfmiddlewaretoken'); + csrfTokenInput.value = getCSRFToken(); + form.appendChild(csrfTokenInput); + document.body.appendChild(form); + form.submit(); + } + }); +} diff --git a/base/static/cbv/shift_request/shift_request_bulk_actions.js b/base/static/cbv/shift_request/shift_request_bulk_actions.js new file mode 100644 index 000000000..5cbd39657 --- /dev/null +++ b/base/static/cbv/shift_request/shift_request_bulk_actions.js @@ -0,0 +1,455 @@ + + +var excelMessages = { + ar: "هل ترغب في تنزيل ملف Excel؟", + de: "Möchten Sie die Excel-Datei herunterladen?", + es: "¿Desea descargar el archivo de Excel?", + en: "Do you want to download the excel file?", + fr: "Voulez-vous télécharger le fichier Excel?", + }; + + var archiveMessages = { + ar: "هل ترغب حقًا في أرشفة كل الحضور المحدد؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten archivieren?", + es: "Realmente quieres archivar todas las asistencias seleccionadas?", + en: "Do you really want to archive all the selected requests?", + fr: "Voulez-vous vraiment archiver toutes les présences sélectionnées?", + }; + + var unarchiveMessages = { + ar: "هل ترغب حقًا في إلغاء أرشفة كل الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten archivierten Zuweisungen wiederherstellen?", + es: "Realmente quieres desarchivar todas las asignaciones seleccionadas?", + en: "Do you really want to un-archive all the selected requests?", + fr: "Voulez-vous vraiment désarchiver toutes les allocations sélectionnées?", + }; + + var shiftDeleteRequestMessages = { + ar: "هل ترغب حقًا في حذف كل الحجوزات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Zuweisungen löschen?", + es: "Realmente quieres eliminar todas las asignaciones seleccionadas?", + en: "Do you really want to delete all the selected requests?", + fr: "Voulez-vous vraiment supprimer toutes les allocations sélectionnées?", + }; + + var approveMessages = { + ar: "هل ترغب حقًا في الموافقة على جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen genehmigen?", + es: "Realmente quieres aprobar todas las solicitudes seleccionadas?", + en: "Do you really want to approve all the selected requests?", + fr: "Voulez-vous vraiment approuver toutes les demandes sélectionnées?", + }; + var rejectMessages = { + ar: "هل تريد حقًا رفض جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen ablehnen?", + es: "¿Realmente deseas rechazar todas las solicitudes seleccionadas?", + en: "Do you really want to reject all the selected requests?", + fr: "Voulez-vous vraiment rejeter toutes les demandes sélectionnées?", + }; + var requestDeleteMessages = { + ar: "هل ترغب حقًا في حذف جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen löschen?", + es: "Realmente quieres eliminar todas las solicitudes seleccionadas?", + en: "Do you really want to delete all the selected requests?", + fr: "Voulez-vous vraiment supprimer toutes les demandes sélectionnées?", + }; + var norowMessagesShift = { + ar: "لم يتم تحديد أي صفوف.", + de: "Es wurden keine Zeilen ausgewählt.", + es: "No se han seleccionado filas.", + en: "No rows have been selected.", + fr: "Aucune ligne n'a été sélectionnée.", + }; + var rowMessages = { + ar: " تم الاختيار", + de: " Ausgewählt", + es: " Seleccionado", + en: " Selected", + fr: " Sélectionné", + }; + + tickShiftCheckboxes(); + function makeShiftListUnique(list) { + return Array.from(new Set(list)); + } + + tickWorktypeCheckboxes(); + function makeWorktypeListUnique(list) { + return Array.from(new Set(list)); + } + + tickRShiftCheckboxes(); + function makeRShiftListUnique(list) { + return Array.from(new Set(list)); + } + + tickRWorktypeCheckboxes(); + function makeRWorktypeListUnique(list) { + return Array.from(new Set(list)); + } + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + var languageCode = $("#main-section-data").attr("data-lang"); + var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"]; + if (allowedLanguageCodes.includes(languageCode)) { + callback(languageCode); + } else { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var ajaxLanguageCode = response.language_code; + $("#main-section-data").attr("data-lang", ajaxLanguageCode); + callback( + allowedLanguageCodes.includes(ajaxLanguageCode) + ? ajaxLanguageCode + : "en" + ); + }, + error: function () { + callback("en"); + }, + }); + } + } + + function shiftRequestApprove() { + + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = approveMessages[languageCode]; + var textMessage = norowMessagesShift[languageCode]; + ids = []; + // function addIdsTab(tabId){ + // var dataIds = $("#"+tabId).attr("data-ids"); + // if (dataIds){ + // ids = ids.concat(JSON.parse(dataIds)); + // } + // } + // addIdsTab("shiftselectedInstances"); + // addIdsTab("allocatedselectedInstances"); + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + // Use SweetAlert for the confirmation dialog + Swal.fire({ + text: confirmMessage, + icon: "success", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + // ids = []; + // ids.push($("#selectedInstances").attr("data-ids")); + // ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/shift-request-bulk-approve", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + function shiftRequestReject() { + + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = rejectMessages[languageCode]; + var textMessage = norowMessagesShift[languageCode]; + ids = []; + // function addIdsTab(tabId){ + // var dataIds = $("#"+tabId).attr("data-ids"); + // if (dataIds){ + // ids = ids.concat(JSON.parse(dataIds)); + // } + // } + // addIdsTab("shiftselectedInstances"); + // addIdsTab("allocatedselectedInstances"); + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + $.ajax({ + type: "POST", + url: "/shift-request-bulk-cancel", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + function shiftRequestDelete() { + + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = shiftDeleteRequestMessages[languageCode]; + var textMessage = norowMessagesShift[languageCode]; + ids = []; + // function addIdsTab(tabId){ + // var dataIds = $("#"+tabId).attr("data-ids"); + // if (dataIds){ + // ids = ids.concat(JSON.parse(dataIds)); + // } + // } + // addIdsTab("shiftselectedInstances"); + // addIdsTab("allocatedselectedInstances"); + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + $.ajax({ + type: "POST", + url: "/shift-request-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + + function archiveRotateShift() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = archiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-shift-assign-bulk-archive?is_active=False", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }; + + function un_archiveRotateShift() { + + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = unarchiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-shift-assign-bulk-archive?is_active=True", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }; + + function deleteRotatingShift() { + + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = shiftDeleteRequestMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-shift-assign-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }; + \ No newline at end of file diff --git a/base/static/cbv/work_type_request/work_request_bulk_action.js b/base/static/cbv/work_type_request/work_request_bulk_action.js new file mode 100644 index 000000000..ad4653a97 --- /dev/null +++ b/base/static/cbv/work_type_request/work_request_bulk_action.js @@ -0,0 +1,433 @@ +var excelMessages = { + ar: "هل ترغب في تنزيل ملف Excel؟", + de: "Möchten Sie die Excel-Datei herunterladen?", + es: "¿Desea descargar el archivo de Excel?", + en: "Do you want to download the excel file?", + fr: "Voulez-vous télécharger le fichier Excel?", + }; + + var archiveMessages = { + ar: "هل ترغب حقًا في أرشفة كل الحضور المحدد؟", + de: "Möchten Sie wirklich alle ausgewählten Anwesenheiten archivieren?", + es: "Realmente quieres archivar todas las asistencias seleccionadas?", + en: "Do you really want to archive all the selected allocations?", + fr: "Voulez-vous vraiment archiver toutes les présences sélectionnées?", + }; + + var unarchiveMessages = { + ar: "هل ترغب حقًا في إلغاء أرشفة كل الحضور المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten archivierten Zuweisungen wiederherstellen?", + es: "Realmente quieres desarchivar todas las asignaciones seleccionadas?", + en: "Do you really want to un-archive all the selected allocations?", + fr: "Voulez-vous vraiment désarchiver toutes les allocations sélectionnées?", + }; + + var rotatedeleteRequestMessages = { + ar: "هل ترغب حقًا في حذف كل الحجوزات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Zuweisungen löschen?", + es: "Realmente quieres eliminar todas las asignaciones seleccionadas?", + en: "Do you really want to delete all the selected Requests?", + fr: "Voulez-vous vraiment supprimer toutes les allocations sélectionnées?", + }; + + var approveMessages = { + ar: "هل ترغب حقًا في الموافقة على جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen genehmigen?", + es: "Realmente quieres aprobar todas las solicitudes seleccionadas?", + en: "Do you really want to approve all the selected requests?", + fr: "Voulez-vous vraiment approuver toutes les demandes sélectionnées?", + }; + var rejectMessages = { + ar: "هل تريد حقًا رفض جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen ablehnen?", + es: "¿Realmente deseas rechazar todas las solicitudes seleccionadas?", + en: "Do you really want to reject all the selected requests?", + fr: "Voulez-vous vraiment rejeter toutes les demandes sélectionnées?", + }; + var requestDeleteMessages = { + ar: "هل ترغب حقًا في حذف جميع الطلبات المحددة؟", + de: "Möchten Sie wirklich alle ausgewählten Anfragen löschen?", + es: "Realmente quieres eliminar todas las solicitudes seleccionadas?", + en: "Do you really want to delete all the selected requests?", + fr: "Voulez-vous vraiment supprimer toutes les demandes sélectionnées?", + }; + var norowMessages = { + ar: "لم يتم تحديد أي صفوف.", + de: "Es wurden keine Zeilen ausgewählt.", + es: "No se han seleccionado filas.", + en: "No rows have been selected.", + fr: "Aucune ligne n'a été sélectionnée.", + }; + var rowMessages = { + ar: " تم الاختيار", + de: " Ausgewählt", + es: " Seleccionado", + en: " Selected", + fr: " Sélectionné", + }; + + tickShiftCheckboxes(); + function makeShiftListUnique(list) { + return Array.from(new Set(list)); + } + + tickWorktypeCheckboxes(); + function makeWorktypeListUnique(list) { + return Array.from(new Set(list)); + } + + tickRShiftCheckboxes(); + function makeRShiftListUnique(list) { + return Array.from(new Set(list)); + } + + tickRWorktypeCheckboxes(); + function makeRWorktypeListUnique(list) { + return Array.from(new Set(list)); + } + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + var languageCode = $("#main-section-data").attr("data-lang"); + var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"]; + if (allowedLanguageCodes.includes(languageCode)) { + callback(languageCode); + } else { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var ajaxLanguageCode = response.language_code; + $("#main-section-data").attr("data-lang", ajaxLanguageCode); + callback( + allowedLanguageCodes.includes(ajaxLanguageCode) + ? ajaxLanguageCode + : "en" + ); + }, + error: function () { + callback("en"); + }, + }); + } + } + + + function handleApproveRequestsClick() { + // e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = approveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "success", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/work-type-request-bulk-approve", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + + +function handleRejectRequestsClick() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = rejectMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + console.log("hello") + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/work-type-request-bulk-cancel", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + function handleDeleteRequestsClick() { + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = rotatedeleteRequestMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/work-type-request-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + //scripts for rotating work type page + //bulk archive in rotate work type + $(document).on('click', '#archiveWorkRotateNav', function(e) { + + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = archiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-work-type-assign-bulk-archive?is_active=False", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); + + + function UnarchiveWorkRotateNav() { + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = unarchiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-work-type-assign-bulk-archive?is_active=True", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + + function deleteWorkRotateNav() { + + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = rotatedeleteRequestMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + if (ids.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + ids = []; + ids.push($("#selectedInstances").attr("data-ids")); + ids = JSON.parse($("#selectedInstances").attr("data-ids")); + $.ajax({ + type: "POST", + url: "/rotating-work-type-assign-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + } + diff --git a/base/static/holiday/action.js b/base/static/holiday/action.js index 5c833923a..86ebe454e 100644 --- a/base/static/holiday/action.js +++ b/base/static/holiday/action.js @@ -325,7 +325,7 @@ $("#bulkHolidaysDelete").click(function (e) { }); }); -$(".holidaysInfoImport").click(function (e) { +$(document).on("click", "#holidaysInfoImport", function (e) { e.preventDefault(); var languageCode = null; getCurrentLanguageCode(function (code) { diff --git a/base/templates/base/action_type/action_type.html b/base/templates/base/action_type/action_type.html index c818c40f6..29ecfc985 100644 --- a/base/templates/base/action_type/action_type.html +++ b/base/templates/base/action_type/action_type.html @@ -1,33 +1,39 @@ {% extends 'settings.html' %} {% load i18n %} {% block settings %}{% load static %} +{% include "generic/components.html" %} + +
    -
    -

    {% trans "Action Type" %}

    - {% if perms.employee.add_actiontype %} - - {% endif %} +
    + +
    +
    - {% if action_types %} -
    - {% include 'base/action_type/action_type_view.html' %} + {% comment %} +
    +

    {% trans "Action Type" %}

    + {% if perms.employee.add_actiontype %} + + {% endif %}
    - {% else %} + {% if action_types %} + {% include 'base/action_type/action_type_view.html' %} + {% else %}
    - Page not found. 404. + Page not found. 404.
    {% trans "There is no disciplinary action type at this moment." %}
    - {% endif %} + {% endif %} + {% endcomment %}
    - + {% endblock settings %} diff --git a/base/templates/base/auth/permission_lines.html b/base/templates/base/auth/permission_lines.html index 82e6d8ab1..d4aedc5a3 100644 --- a/base/templates/base/auth/permission_lines.html +++ b/base/templates/base/auth/permission_lines.html @@ -1,6 +1,6 @@ {% load basefilters i18n %} {% for employee in employees %} -
    +
    diff --git a/base/templates/base/company/company.html b/base/templates/base/company/company.html index 0515829f7..e3d842726 100644 --- a/base/templates/base/company/company.html +++ b/base/templates/base/company/company.html @@ -1,4 +1,7 @@ {% extends 'settings.html' %} {% load i18n %} {% block settings %}{% load static %} +{% include "generic/components.html" %} + +
    {% if perms.base.view_company %}
    +
    {% if perms.base.view_department %} -
    +
    + {% comment %}

    {% trans "Department" %}

    {% if perms.base.add_department %}
    {% endcomment %} + +
    +
    - {% if departments %} + {% comment %} {% if departments %} {% include 'base/department/department_view.html' %} {% else %}
    @@ -20,7 +28,7 @@ src="{% static 'images/ui/connection.png' %}" class="" alt="Page not found. 404." />
    {% trans "There is no department at this moment." %}
    - {% endif %} + {% endif %} {% endcomment %} {% endif %}
    {% endblock settings %} diff --git a/base/templates/base/employee_type/employee_type.html b/base/templates/base/employee_type/employee_type.html index a86fbf644..f5d37c611 100644 --- a/base/templates/base/employee_type/employee_type.html +++ b/base/templates/base/employee_type/employee_type.html @@ -1,43 +1,40 @@ {% extends 'settings.html' %} {% load i18n %} {% block settings %} {% load static %} +{% include "generic/components.html" %} + +
    - {% if perms.base.view_employeetype %} -
    -

    {% trans "Employee Type" %}

    - {% if perms.base.add_employeetype %} - + {% if perms.base.view_employeetype %} +
    + +
    +
    +
    {% endif %} -
    - {% if employee_types %} - {% include 'base/employee_type/type_view.html' %} - {% else %} -
    - Page not found. 404. -
    {% trans "There is no employee type at this moment." %}
    -
    - {% endif %} - - {% endif %} + {% comment %} +
    +

    {% trans "Employee Type" %}

    + {% if perms.base.add_employeetype %} + + {% endif %} +
    + {% if employee_types %} + {% include 'base/employee_type/type_view.html' %} + {% else %} +
    + Page not found. 404. +
    {% trans "There is no employee type at this moment." %}
    +
    + {% endif %} + {% endcomment %}
    -
    @@ -552,12 +555,6 @@
    {{feedback.end_date}} - +
    {{feedback.end_date}} - +
    {{feedback.end_date}} - +
    {% if perms.pms.change_employeeobjective or instance|is_manager:request.user %} - + {% trans "Edit" %} diff --git a/pms/templates/okr/emp_objective/emp_objective_dashboard_view.html b/pms/templates/okr/emp_objective/emp_objective_dashboard_view.html index 53a15d365..419f25405 100644 --- a/pms/templates/okr/emp_objective/emp_objective_dashboard_view.html +++ b/pms/templates/okr/emp_objective/emp_objective_dashboard_view.html @@ -48,10 +48,10 @@ {% comment %} {% if request.user|is_reportingmanager or perms.pms.view_employeekeyresult %} {% endcomment %} @@ -72,9 +72,9 @@ + {% endif %} + {% if request.user.employee_get in emp_objective.objective_id.managers.all or perms.pms.add_employeeobjective %} -
    - - - - {% if request.user.employee_get in emp_objective.objective_id.managers.all or emp_objective.employee_id == request.user.employee_get or perms.pms.view_employeekeyresult %} - - - - {% endif %} - {% if request.user.employee_get in emp_objective.objective_id.managers.all or perms.pms.add_employeekeyresult %} - - {% endif %} - {% if request.user.employee_get in emp_objective.objective_id.managers.all or perms.pms.add_employeeobjective %} - -
    -
    - -
    - {% endif %} - -
    + {% endif %} +
    @@ -173,7 +171,6 @@ class="oh-accordion-meta__body d-none" id="krBody{{emp_objective.id}}" > -
    + .filter-pill { + border-radius: 999px; + padding: 6px 16px; + background-color: #f0f0f0; + display: inline-block; + margin: 4px; + cursor: pointer; + transition: background-color 0.3s ease; + border: none; + } + .filter-pill.active { + background-color: #007bff; + color: white; + } +
    + + + +
    +
    + + {{emp_obj_form.form.progress_percentage__gte}} +
    +
    +
    +
    + + {{emp_obj_form.form.progress_percentage__lte}} +
    +
    +
    +
    {% trans "Key Results" %}
    +
    +
    +
    +
    + + {{emp_obj_form.form.kr_start_date_from}} +
    +
    +
    +
    + + {{emp_obj_form.form.kr_start_date_till}} +
    +
    +
    +
    + + {{emp_obj_form.form.kr_end_date_from}} +
    +
    +
    +
    + + {{emp_obj_form.form.kr_end_date_till}} +
    +
    +
    +
    + + {{emp_obj_form.form.kr_progress_percentage__gte}} +
    +
    +
    +
    + + {{emp_obj_form.form.kr_progress_percentage__lte}} +
    +
    +
    +
    + +
    + + {% for value, label in emp_obj_form.form.due.field.choices %} + {% if value %} + + {% endif %} + {% endfor %} +
    +
    +
    +
    +
    +
    {% for kr in krs.object_list %} +
    @@ -73,8 +74,9 @@
    diff --git a/pms/templates/okr/okr_detailed_view.html b/pms/templates/okr/okr_detailed_view.html index dee1d80ec..e6264303b 100644 --- a/pms/templates/okr/okr_detailed_view.html +++ b/pms/templates/okr/okr_detailed_view.html @@ -1,205 +1,136 @@ {% extends 'index.html' %} {% load static i18n %} {% load i18n %} {% load widget_tweaks %} {% block content %} {% load basefilters %} {% include 'filter_tags.html' %} - {% include "okr/emp_objective/emp_objective_nav.html" %} +{% include "generic/components.html" %}
    -
    -
    -
    -
    -

    {{objective}}

    - -
    - {% if perms.pms.change_objective %} - - {% endif %} {% if perms.pms.add_employeeobjective %} - - {% endif %} -
    -
    -
    - -
      -
    • - {% trans "Managers:" %} - -
      -
      - {% for manager in objective.managers.all %} - - {% endfor %} -
      +
      +
      +
      +
      +
      +

      {{objective}}

      + +
      + {% if perms.pms.change_objective %} + + {% endif %} {% if perms.pms.add_employeeobjective or request.user.employee_get in objective.managers.all %} + + {% endif %}
      -
    • -
    • - {% trans "Duration:" %} - {{objective.duration}} - {{objective.get_duration_unit_display}} -
    • -
    • - {% trans "Description:" %} - {{objective.description}} -
    • -
    +
    + +
      +
    • + {% trans "Managers:" %} + +
      +
      + {% for manager in objective.managers.all %} + + {% endfor %} +
      +
      +
      +
    • +
    • + {% trans "Duration:" %} + {{objective.duration}} + {{objective.get_duration_unit_display}} +
    • +
    • + {% trans "Description:" %} + {{objective.description}} +
    • +
    +
    -
    - {% if objective.employee_objective.all %} -
    - {% else %} - -
    -
    + {% else %} + +
    -
    - {% trans "There are no assignees for this objective at the moment." %} -
    + > + Page not found. 404. +
    + {% trans "There are no assignees for this objective at the moment." %} +
    +
    + + {% endif %}
    - - {% endif %} -
    - + - + -
    -
    +
    +
    +
    + + +
    - - - " + ) @login_required @@ -528,7 +531,10 @@ def objective_manager_remove(request, obj_id, manager_id): """ objective = get_object_or_404(Objective, id=obj_id) objective.managers.remove(manager_id) - return HttpResponse("") + messages.success(request, _("Manger removed successfully.")) + return HttpResponse( + "" + ) @login_required @@ -547,7 +553,10 @@ def key_result_remove(request, obj_id, kr_id): """ objective = get_object_or_404(Objective, id=obj_id) objective.key_result_id.remove(kr_id) - return HttpResponse("") + messages.success(request, _("Key result removed successfully.")) + return HttpResponse( + "" + ) @login_required @@ -569,8 +578,10 @@ def assignees_remove(request, obj_id, emp_id): EmployeeObjective, employee_id=emp_id, objective_id=obj_id ).delete() objective.assignees.remove(emp_id) - - return HttpResponse() + messages.success(request, _("Assignee removed successfully.")) + return HttpResponse( + "" + ) def objective_filter_pagination(request, objective_own): @@ -856,7 +867,7 @@ def emp_objective_search(request, obj_id): search_val = request.GET.get("search") if search_val is None: search_val = "" - emp_objectives = EmployeeObjectiveFilter(request.GET, emp_objectives).qs + emp_objectives = EmployeeObjectiveFilter(request.GET, emp_objectives).qs.distinct() if not request.GET.get("archive") == "true": emp_objectives = emp_objectives.filter(archive=False) previous_data = request.GET.urlencode() @@ -892,11 +903,11 @@ def kr_table_view(request, emp_objective_id): """ emp_objective = EmployeeObjective.objects.get(id=emp_objective_id) krs = emp_objective.employee_key_result.all() + krs = KeyResultFilter(request.GET, queryset=krs).qs.distinct() krs = Paginator(krs, get_pagination()) krs_page = request.GET.get("krs_page") krs = krs.get_page(krs_page) previous_data = request.GET.urlencode() - context = { "krs": krs, "key_result_status": EmployeeKeyResult.STATUS_CHOICES, @@ -1029,7 +1040,9 @@ def objective_archive(request, id): objective.archive = True objective.save() messages.info(request, _("Objective archived successfully!.")) - return redirect(f"/pms/objective-list-view?{request.environ['QUERY_STRING']}") + return HttpResponse( + "" + ) @login_required @@ -1165,6 +1178,10 @@ def archive_employee_objective(request, emp_obj_id): emp_objective.archive = True emp_objective.save() messages.success(request, _("Objective archived successfully!.")) + if request.GET.get("detail_view"): + return HttpResponse( + "" + ) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) @@ -1208,6 +1225,19 @@ def change_employee_objective_status(request): """ emp_obj = request.GET.get("empObjId") emp_objective = EmployeeObjective.objects.filter(id=emp_obj).first() + if not ( + request.user.has_perm("pms.change_objective") + or request.user.has_perm("pms.change_employeeobjective") + or request.user.has_perm("pms.change_employeekeyresult") + or request.user.employee_get in emp_objective.objective_id.managers.all() + or ( + emp_objective.objective_id.self_employee_progress_update + and (emp_objective.employee_id == request.user.employee_get) + ) + ): + messages.info(request, "You dont have permission") + return HttpResponse("") + status = request.GET.get("status") if not ( request.user.has_perm("pms.change_objective") @@ -1473,11 +1503,13 @@ def feedback_creation(request): it will return feedback creation html. """ form = FeedbackForm() + form.fields["manager_id"].required = False context = { "feedback_form": form, } if request.method == "POST": form = FeedbackForm(request.POST) + form.fields["manager_id"].required = False if form.is_valid(): employees = form.data.getlist("subordinate_id") if key_result_ids := request.POST.getlist("employee_key_results_id"): @@ -1492,7 +1524,7 @@ def feedback_creation(request): messages.success(request, _("Feedback created successfully.")) send_feedback_notifications(request, feedback=instance) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) else: context["feedback_form"] = form return render(request, "feedback/feedback_creation.html", context) @@ -1853,12 +1885,12 @@ def feedback_answer_get(request, id, **kwargs): # check if the feedback start_date is not started yet if feedback.start_date > datetime.date.today(): messages.info(request, _("Feedback not started yet")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) # check if the feedback end_date is not over if feedback.end_date and feedback.end_date < datetime.date.today(): - messages.info(request, _("Feedback is due")) - return redirect(feedback_list_view) + messages.info(request, _("Feedback is due/closed")) + return redirect(reverse("feedback-view")) user = request.user employee = Employee.objects.filter(employee_user_id=user).first() answer = Answer.objects.filter(feedback_id=feedback, employee_id=employee) @@ -1874,7 +1906,7 @@ def feedback_answer_get(request, id, **kwargs): ) if not employee in feedback_employees: messages.info(request, _("You are not allowed to answer")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) # Employee does not have an answer object for employee in feedback_employees: @@ -1888,7 +1920,7 @@ def feedback_answer_get(request, id, **kwargs): # Check if the feedback has already been answered if answer: messages.info(request, _("Feedback already answered")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) context = { "questions": questions, @@ -1941,7 +1973,7 @@ def feedback_answer_post(request, id): _("Feedback %(review_cycle)s has been answered successfully!.") % {"review_cycle": feedback.review_cycle}, ) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) @login_required @@ -1964,7 +1996,7 @@ def feedback_answer_view(request, id, **kwargs): if not answers: messages.info(request, _("Feedback is not answered yet")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) context = { "answers": answers, @@ -2005,13 +2037,13 @@ def feedback_delete(request, id): _("You can't delete feedback %(review_cycle)s with status %(status)s") % {"review_cycle": feedback.review_cycle, "status": feedback.status}, ) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) except Feedback.DoesNotExist: messages.error(request, _("Feedback not found.")) except ProtectedError: messages.error(request, _("Related entries exists")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) @login_required @@ -2099,6 +2131,7 @@ def feedback_archive(request, id): """ feedback = Feedback.objects.get(id=id) + if feedback.archive: feedback.archive = False feedback.save() @@ -2107,7 +2140,7 @@ def feedback_archive(request, id): feedback.archive = True feedback.save() messages.info(request, _("Feedback archived successfully!.")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) @login_required @@ -2585,6 +2618,7 @@ def period_delete(request, period_id): Returns: it will redirect to period_view. """ + target = request.META.get("HTTP_HX_TARGET") try: obj_period = Period.objects.get(id=period_id) obj_period.delete() @@ -2593,6 +2627,8 @@ def period_delete(request, period_id): messages.error(request, _("Period not found.")) except ProtectedError: messages.error(request, _("Related entries exists")) + if target == "listContainer": + return HttpResponse("") return redirect("period-hx-view") @@ -2840,6 +2876,7 @@ def feedback_bulk_archive(request): """ This method is used to archive/un-archive bulk feedbacks """ + ids = request.POST["ids"] announy_ids = request.POST["announy_ids"] ids = json.loads(ids) @@ -2879,6 +2916,8 @@ def feedback_bulk_delete(request): """ ids = request.POST["ids"] ids = json.loads(ids) + announy_ids = request.POST["announy_ids"] + announy_ids = json.loads(announy_ids) for feedback_id in ids: try: feedback = Feedback.objects.get(id=feedback_id) @@ -2903,6 +2942,17 @@ def feedback_bulk_delete(request): except Feedback.DoesNotExist: messages.error(request, _("Feedback not found.")) + for feedback_id in announy_ids: + feedback_id = AnonymousFeedback.objects.get(id=feedback_id) + message = _("Deleted") + # feedback_id.archive = is_active + feedback_id.delete() + messages.success( + request, + _("{feedback} is {message}").format( + feedback=feedback_id.feedback_subject, message=message + ), + ) return JsonResponse({"message": "Success"}) @@ -3119,7 +3169,7 @@ def delete_anonymous_feedback(request, obj_id): except ProtectedError: messages.error(request, _("Related entries exists")) - return redirect(feedback_list_view) + return redirect(reverse("feedback-view")) @login_required @@ -3271,7 +3321,7 @@ def delete_employee_keyresult(request, kr_id): # objective.assignees.remove(employee) messages.success(request, _("Objective deleted successfully!.")) if request.GET.get("dashboard"): - return redirect(f"/pms/dashboard-view") + return HttpResponse("") return redirect(f"/pms/objective-detailed-view/{objective.id}") @@ -3285,13 +3335,27 @@ def employee_keyresult_update_status(request, kr_id): redirect to detailed of employee objective """ emp_kr = EmployeeKeyResult.objects.get(id=kr_id) - status = request.POST.get("key_result_status") - emp_kr.status = status - emp_kr.save() - messages.success(request, _("Key result sattus changed to {}.").format(status)) - return redirect( - f"/pms/kr-table-view/{emp_kr.employee_objective_id.id}?&objective_id={emp_kr.employee_objective_id.objective_id.id}" - ) + if ( + request.user.has_perm("pms.change_objective") + or request.user.has_perm("pms.change_employeeobjective") + or request.user.has_perm("pms.change_employeekeyresult") + or request.user.employee_get + in emp_kr.employee_objective_id.objective_id.managers.all() + or ( + emp_kr.employee_objective_id.objective_id.self_employee_progress_update + and (emp_kr.employee_id == request.user.employee_get) + ) + ): + status = request.POST.get("key_result_status") + emp_kr.status = status + emp_kr.save() + messages.success(request, _("Key result sattus changed to {}.").format(status)) + return redirect( + f"/pms/kr-table-view/{emp_kr.employee_objective_id.id}?&objective_id={emp_kr.employee_objective_id.objective_id.id}" + ) + + messages.info(request, "You dont have permission") + return HttpResponse("") @login_required @@ -3317,13 +3381,25 @@ def key_result_current_value_update(request): ) ) ): + current_value = max(0, current_value) emp_kr.current_value = current_value emp_kr.save() emp_kr.employee_objective_id.update_objective_progress() - return JsonResponse({"type": "sucess"}) + messages.success(request, "Value updated") else: - messages.info(request, "You dont have permission") - except: + messages.info( + request, "You dont have permission to update the current value" + ) + return JsonResponse( + { + "type": "sucess", + "progress": emp_kr.employee_objective_id.progress_percentage, + "kr_progress": emp_kr.progress_percentage, + "pk": emp_kr.employee_objective_id.pk, + } + ) + except Exception as e: + print(e) return JsonResponse({"type": "error"}) @@ -3546,7 +3622,7 @@ def meeting_employee_remove(request, meet_id, employee_id): messages.success( request, _("Employee has been successfully removed from the meeting.") ) - return HttpResponse("") + return HttpResponse("") @login_required @@ -3679,7 +3755,7 @@ def meeting_answer_post(request, id): _("Questions for meeting %(meeting)s has been answered successfully!.") % {"meeting": meeting.title}, ) - return redirect(view_meetings) + return redirect(reverse("view-meetings")) @login_required @@ -3746,7 +3822,7 @@ def meeting_single_view(request, id): @login_required @hx_request_required @owner_can_enter("pms.view_feedback", Employee) -def performance_tab(request, emp_id): +def performance_tab(request, pk): """ This function is used to view performance tab of an employee in employee individual & profile view. @@ -3758,7 +3834,7 @@ def performance_tab(request, emp_id): Returns: return performance-tab template """ - feedback_own = Feedback.objects.filter(employee_id=emp_id, archive=False) + feedback_own = Feedback.objects.filter(employee_id=pk, archive=False) today = datetime.datetime.today() context = { diff --git a/project/cbv/dashboard.py b/project/cbv/dashboard.py index 40ccc2220..5c8deee8d 100644 --- a/project/cbv/dashboard.py +++ b/project/cbv/dashboard.py @@ -10,7 +10,6 @@ from django.db.models import Count, Q from django.db.models.query import QuerySet from django.urls import resolve, reverse from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from base.methods import get_subordinates @@ -71,6 +70,16 @@ class ProjectDetailView(HorillaDetailedView): model = Project title = _("Details") header = {"title": "title", "subtitle": "", "avatar": "get_avatar"} + body = [ + (_("Manager"), "manager"), + (_("Members"), "get_members"), + (_("Status"), "status_column"), + (_("No of Tasks"), "task_count"), + (_("Start date"), "start_date"), + (_("End date"), "end_date"), + (_("Document"), "get_document_html"), + (_("Description"), "description"), + ] def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -97,17 +106,3 @@ class ProjectDetailView(HorillaDetailedView): queryset = super().get_queryset() queryset = queryset.annotate(task_count=Count("task")) return queryset - - @cached_property - def body(self): - get_field = self.model()._meta.get_field - return [ - (get_field("managers").verbose_name, "get_managers"), - (get_field("members").verbose_name, "get_members"), - (get_field("status").verbose_name, "get_status_display"), - (_("No of Tasks"), "task_count"), - (get_field("start_date").verbose_name, "start_date"), - (get_field("end_date").verbose_name, "end_date"), - (get_field("document").verbose_name, "get_document_html"), - (get_field("description").verbose_name, "description"), - ] diff --git a/project/cbv/projects.py b/project/cbv/projects.py index f258e90f6..1890fc552 100644 --- a/project/cbv/projects.py +++ b/project/cbv/projects.py @@ -56,12 +56,7 @@ class ProjectsNavView(HorillaNavView): Nav bar """ - filter_form_context_name = "form" - filter_instance = ProjectFilter() - search_swap_target = "#listContainer" - group_by_fields = ["status", "is_active"] template_name = "cbv/projects/project_nav.html" - filter_body_template = "cbv/projects/filter.html" def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -144,6 +139,16 @@ class ProjectsNavView(HorillaNavView): hx-get="{reverse('create-project')}" """ + group_by_fields = [ + ("status", _("Status")), + ("is_active", _("Is active")), + ] + nav_title = Project._meta.verbose_name_plural + filter_instance = ProjectFilter() + filter_form_context_name = "form" + filter_body_template = "cbv/projects/filter.html" + search_swap_target = "#listContainer" + @method_decorator(login_required, name="dispatch") @method_decorator( @@ -154,9 +159,6 @@ class ProjectsList(HorillaListView): Projects list view """ - model = Project - filter_class = ProjectFilter - def get_queryset(self): queryset = super().get_queryset() if not self.request.user.has_perm("project.view_project"): @@ -176,26 +178,26 @@ class ProjectsList(HorillaListView): @cached_property def columns(self): - get_field = self.model()._meta.get_field + instance = self.model() return [ - (get_field("title").verbose_name, "title"), - (get_field("managers").verbose_name, "get_managers"), - (get_field("members").verbose_name, "get_members"), - (get_field("status").verbose_name, "get_status_display"), - (get_field("start_date").verbose_name, "start_date"), - (get_field("end_date").verbose_name, "end_date"), - (get_field("document").verbose_name, "get_document_html"), - (get_field("description").verbose_name, "get_description"), + (instance._meta.get_field("title").verbose_name, "title"), + (instance._meta.get_field("managers").verbose_name, "get_managers"), + (instance._meta.get_field("members").verbose_name, "get_members"), + (instance._meta.get_field("status").verbose_name, "status_column"), + (instance._meta.get_field("start_date").verbose_name, "start_date"), + (instance._meta.get_field("end_date").verbose_name, "end_date"), + (instance._meta.get_field("document").verbose_name, "get_document_html"), + (instance._meta.get_field("description").verbose_name, "get_description"), ] - @cached_property - def sortby_mapping(self): - get_field = self.model()._meta.get_field - return [ - (get_field("title").verbose_name, "title"), - (get_field("start_date").verbose_name, "start_date"), - (get_field("end_date").verbose_name, "end_date"), - ] + model = Project + filter_class = ProjectFilter + + sortby_mapping = [ + ("Project", "title"), + ("Start Date", "start_date"), + ("End Date", "end_date"), + ] row_status_indications = [ ( @@ -279,8 +281,8 @@ class ProjectFormView(HorillaFormView): form view for create project """ - model = Project form_class = ProjectForm + model = Project new_display_title = _("Create") + " " + model._meta.verbose_name def __init__(self, **kwargs): @@ -391,7 +393,7 @@ class ProjectCardView(HorillaCardView): details = { "image_src": "get_avatar", "title": "{get_task_badge_html}", - "subtitle": "Status : {get_status_display}
    Start date : {start_date}
    End date : {end_date}", + "subtitle": "Status : {status_column}
    Start date : {start_date}
    End date : {end_date}", } card_status_class = "status-{status}" diff --git a/project/cbv/tasks.py b/project/cbv/tasks.py index d729f0560..7d781cde0 100644 --- a/project/cbv/tasks.py +++ b/project/cbv/tasks.py @@ -12,7 +12,6 @@ from django.http import HttpResponse from django.shortcuts import render from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from base.methods import get_subordinates @@ -53,7 +52,6 @@ class TaskListView(HorillaListView): model = Task filter_class = TaskAllFilter - action_method = "actions" def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -83,30 +81,26 @@ class TaskListView(HorillaListView): ) return queryset.distinct() - @cached_property - def columns(self): - get_field = self.model()._meta.get_field - return [ - (get_field("title").verbose_name, "title"), - (get_field("project").verbose_name, "project"), - (get_field("stage").verbose_name, "stage"), - (get_field("task_managers").verbose_name, "get_managers"), - (get_field("task_members").verbose_name, "get_members"), - (get_field("end_date").verbose_name, "end_date"), - (get_field("status").verbose_name, "get_status_display"), - (get_field("description").verbose_name, "get_description"), - ] + columns = [ + (_("Task"), "title"), + (_("Project"), "project"), + (_("Stage"), "stage"), + (_("Mangers"), "get_managers"), + (_("Members"), "get_members"), + (_("End Date"), "end_date"), + (_("Status"), "status_column"), + (_("Description"), "description"), + ] - @cached_property - def sortby_mapping(self): - get_field = self.model()._meta.get_field - return [ - (get_field("title").verbose_name, "title"), - (get_field("project").verbose_name, "project__title"), - (get_field("stage").verbose_name, "stage"), - (get_field("end_date").verbose_name, "end_date"), - (get_field("status").verbose_name, "status"), - ] + sortby_mapping = [ + ("Task", "title"), + ("Project", "project__title"), + ("Stage", "stage"), + ("End Date", "end_date"), + ("Status", "status"), + ] + + action_method = "actions" row_status_indications = [ ( @@ -170,15 +164,11 @@ class TasksNavBar(HorillaNavView): navbar of teh page """ - group_by_fields = [ - "project", - "stage", - "status", - ] - filter_form_context_name = "form" + nav_title = _("Tasks") filter_instance = TaskAllFilter() - search_swap_target = "#listContainer" + filter_form_context_name = "form" filter_body_template = "cbv/tasks/task_filter.html" + search_swap_target = "#listContainer" def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -247,6 +237,12 @@ class TasksNavBar(HorillaNavView): }, ] + group_by_fields = [ + ("project", _("Project")), + ("stage", _("Stage")), + ("status", _("Status")), + ] + @method_decorator(login_required, name="dispatch") class TaskCreateForm(HorillaFormView): @@ -254,10 +250,10 @@ class TaskCreateForm(HorillaFormView): Form view for create and update tasks """ - model = Task form_class = TaskAllForm + model = Task template_name = "cbv/tasks/task_form.html" - new_display_title = _("Create") + " " + model._meta.verbose_name + new_display_title = _("Create Task") def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -411,23 +407,22 @@ class TaskDetailView(HorillaDetailedView): model = Task title = _("Task Details") - action_method = "detail_view_actions" + header = {"title": "title", "subtitle": "project", "avatar": "get_avatar"} - @cached_property - def body(self): - get_field = self.model()._meta.get_field - return [ - (get_field("title").verbose_name, "title"), - (get_field("project").verbose_name, "project"), - (get_field("stage").verbose_name, "stage"), - (get_field("task_managers").verbose_name, "get_managers"), - (get_field("task_members").verbose_name, "get_members"), - (get_field("status").verbose_name, "get_status_display"), - (get_field("end_date").verbose_name, "end_date"), - (get_field("description").verbose_name, "description"), - (get_field("document").verbose_name, "document_col", True), - ] + body = [ + (_("Task"), "title"), + (_("Project"), "project"), + (_("Stage"), "stage"), + (_("Task Mangers"), "get_managers"), + (_("Task Members"), "get_members"), + (_("Status"), "status_column"), + (_("End Date"), "end_date"), + (_("Description"), "description"), + (_("Document"), "document_col", True), + ] + + action_method = "detail_view_actions" @method_decorator(login_required, name="dispatch") diff --git a/project/cbv/timesheet.py b/project/cbv/timesheet.py index 296b6ae95..57d9634c3 100644 --- a/project/cbv/timesheet.py +++ b/project/cbv/timesheet.py @@ -11,7 +11,6 @@ from django.http import HttpResponse from django.shortcuts import render from django.urls import resolve, reverse from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from employee.models import Employee @@ -53,22 +52,7 @@ class TimeSheetNavView(HorillaNavView): Nav bar """ - filter_form_context_name = "form" - filter_instance = TimeSheetFilter() - search_swap_target = "#listContainer" template_name = "cbv/timesheet/timesheet_nav.html" - filter_body_template = "cbv/timesheet/filter.html" - group_by_fields = [ - "employee_id", - "project_id", - "date", - "status", - "employee_id__employee_work_info__reporting_manager_id", - "employee_id__employee_work_info__department_id", - "employee_id__employee_work_info__job_position_id", - "employee_id__employee_work_info__employee_type_id", - "employee_id__employee_work_info__company_id", - ] def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -121,6 +105,27 @@ class TimeSheetNavView(HorillaNavView): hx-get="{reverse('create-time-sheet')}" """ + group_by_fields = [ + ("employee_id", _("Employee")), + ("project_id", _("Project")), + ("date", _("Date")), + ("status", _("Status")), + ( + "employee_id__employee_work_info__reporting_manager_id", + _("Reporting Manager"), + ), + ("employee_id__employee_work_info__department_id", _("Department")), + ("employee_id__employee_work_info__job_position_id", _("Job Position")), + ("employee_id__employee_work_info__employee_type_id", _("Employement Type")), + ("employee_id__employee_work_info__company_id", _("Company")), + ] + + nav_title = _("Time Sheet") + filter_instance = TimeSheetFilter() + filter_form_context_name = "form" + filter_body_template = "cbv/timesheet/filter.html" + search_swap_target = "#listContainer" + @method_decorator(login_required, name="dispatch") @method_decorator( @@ -131,9 +136,6 @@ class TimeSheetList(HorillaListView): Time sheet list view """ - model = TimeSheet - filter_class = TimeSheetFilter - def get_queryset(self): queryset = super().get_queryset() if not self.request.user.has_perm("project.view_timesheet"): @@ -151,37 +153,26 @@ class TimeSheetList(HorillaListView): self.search_url = reverse("time-sheet-list") self.action_method = "actions" - @cached_property - def columns(self): - get_field = self.model()._meta.get_field - return [ - ( - get_field("employee_id").verbose_name, - "employee_id", - "employee_id__get_avatar", - ), - (get_field("project_id").verbose_name, "project_id"), - (get_field("task_id").verbose_name, "task_id"), - (get_field("date").verbose_name, "date"), - (get_field("time_spent").verbose_name, "time_spent"), - (get_field("status").verbose_name, "get_status_display"), - (get_field("description").verbose_name, "description"), - ] + model = TimeSheet + filter_class = TimeSheetFilter - @cached_property - def sortby_mapping(self): - get_field = self.model()._meta.get_field - return [ - ( - get_field("employee_id").verbose_name, - "employee_id__employee_first_name", - "employee_id__get_avatar", - ), - (get_field("project_id").verbose_name, "project_id__title"), - (get_field("task_id").verbose_name, "task_id__title"), - (get_field("time_spent").verbose_name, "time_spent"), - (get_field("date").verbose_name, "date"), - ] + columns = [ + (_("Employee"), "employee_id", "employee_id__get_avatar"), + (_("Project"), "project_id"), + (_("Task"), "task_id"), + (_("Date"), "date"), + (_("Time Spent"), "time_spent"), + (_("Status"), "status_column"), + (_("Description"), "description"), + ] + + sortby_mapping = [ + ("Employee", "employee_id__employee_first_name", "employee_id__get_avatar"), + ("Project", "project_id__title"), + ("Task", "task_id__title"), + ("Time Spent", "time_spent"), + ("Date", "date"), + ] row_status_indications = [ ( @@ -276,10 +267,6 @@ class TimeSheetFormView(HorillaFormView): form view for create project """ - form_class = TimeSheetForm - model = TimeSheet - new_display_title = _("Create") + " " + model._meta.verbose_name - def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.dynamic_create_fields = [ @@ -292,7 +279,7 @@ class TimeSheetFormView(HorillaFormView): form_class = TimeSheetForm model = TimeSheet - new_display_title = _("Create") + " " + model._meta.verbose_name + new_display_title = _("Create Time Sheet") # template_name = "cbv/timesheet/form.html" def get_initial(self) -> dict: @@ -498,15 +485,13 @@ class TimeSheetDetailView(HorillaDetailedView): "subtitle": "project_id", "avatar": "employee_id__get_avatar", } - action_method = "detail_actions" - @cached_property - def body(self): - get_field = self.model()._meta.get_field - return [ - (get_field("task_id").verbose_name, "task_id"), - (get_field("date").verbose_name, "date"), - (get_field("time_spent").verbose_name, "time_spent"), - (get_field("status").verbose_name, "get_status_display"), - (get_field("description").verbose_name, "description"), - ] + body = [ + (_("Task"), "task_id"), + (_("Date"), "date"), + (_("Time Spent"), "time_spent"), + (_("Status"), "status_column"), + (_("Description"), "description"), + ] + + action_method = "detail_actions" diff --git a/project/filters.py b/project/filters.py index 00e48e762..8e6263ea9 100644 --- a/project/filters.py +++ b/project/filters.py @@ -77,7 +77,6 @@ class TaskAllFilter(HorillaFilterSet): field_name="end_date", lookup_expr="lte", widget=forms.DateInput(attrs={"type": "date"}), - label=_("End Till"), ) class Meta: @@ -92,12 +91,6 @@ class TaskAllFilter(HorillaFilterSet): "status", ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form.fields["end_till"].label = ( - f"{self.Meta.model()._meta.get_field('end_date').verbose_name} Till" - ) - def filter_by_task(self, queryset, _, value): queryset = queryset.filter(title__icontains=value) return queryset @@ -147,11 +140,6 @@ class TimeSheetFilter(HorillaFilterSet): "status", ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form.fields["start_from"].label = _("Start Date From") - self.form.fields["end_till"].label = _("End Date Till") - def filter_by_employee(self, queryset, _, value): """ Filter queryset by first name or last name. diff --git a/project/models.py b/project/models.py index cba958850..bbf777780 100644 --- a/project/models.py +++ b/project/models.py @@ -236,6 +236,9 @@ class Project(HorillaModel): def __str__(self): return self.title + def status_column(self): + return dict(self.PROJECT_STATUS).get(self.status) + class Meta: """ Meta class to add the additional info @@ -537,7 +540,7 @@ class TimeSheet(HorillaModel): on_delete=models.CASCADE, verbose_name=_("Employee"), ) - date = models.DateField(default=timezone.now, verbose_name=_("Date")) + date = models.DateField(default=timezone.now) time_spent = models.CharField( null=True, default="00:00", @@ -632,5 +635,5 @@ class TimeSheet(HorillaModel): return url class Meta: - verbose_name = _("Time Sheet") - verbose_name_plural = _("Time Sheets") + verbose_name = _("TimeSheet") + verbose_name_plural = _("TimeSheets") diff --git a/project/templates/cbv/tasks/task_filter.html b/project/templates/cbv/tasks/task_filter.html index 6e8e33bf5..13d8f1ae3 100644 --- a/project/templates/cbv/tasks/task_filter.html +++ b/project/templates/cbv/tasks/task_filter.html @@ -9,31 +9,32 @@
    - - {{form.task_managers}} + + {{form.task_manager}}
    - + {{form.stage}}
    - + {{form.project}}
    - + {{form.status}}
    - + {{form.end_till}}
    + +
    diff --git a/project/templates/cbv/timesheet/filter.html b/project/templates/cbv/timesheet/filter.html index daa4e40b3..abd72ebea 100644 --- a/project/templates/cbv/timesheet/filter.html +++ b/project/templates/cbv/timesheet/filter.html @@ -6,29 +6,29 @@
    - + {{form.project_id}}
    - + {{form.status}}
    - + {{form.task}}
    - + {{form.date}}
    {% if perms.project.view_timesheet or request.user|is_reportingmanager %}
    - + {{form.employee_id}}
    @@ -43,13 +43,13 @@
    - + {{form.start_from}}
    - + {{form.end_till}}
    diff --git a/project/templates/task/new/filter_task.html b/project/templates/task/new/filter_task.html index 614082c01..14c5018a0 100644 --- a/project/templates/task/new/filter_task.html +++ b/project/templates/task/new/filter_task.html @@ -7,27 +7,27 @@
    - + {{f.form.task_managers}}
    - + {{f.form.stage}}
    - + {{f.form.task_members}}
    - + {{f.form.status}}
    - + {{f.form.end_till}}
    diff --git a/project/templates/task/new/task_kanban_view.html b/project/templates/task/new/task_kanban_view.html index 64fad9a94..3be8a5c13 100644 --- a/project/templates/task/new/task_kanban_view.html +++ b/project/templates/task/new/task_kanban_view.html @@ -122,8 +122,8 @@
    - - diff --git a/recruitment/accessibility.py b/recruitment/accessibility.py new file mode 100644 index 000000000..4e0eb57b5 --- /dev/null +++ b/recruitment/accessibility.py @@ -0,0 +1,41 @@ +""" +recruitment/accessibility.py + +""" + +from recruitment.templatetags.recruitmentfilters import recruitment_manages + + +def add_candidate_accessibility( + request, instance=None, user_perms=[], *args, **kwargs +) -> bool: + """ + Candidate add accessibility + """ + return ( + request.user.has_perm("recruitment.add_candidate") + or request.user.employee_get in instance.stage_managers.all + or request.user.employee_get in instance.recruitment_id.recruitment_managers.all + ) + + +def edit_stage_accessibility( + request, instance=None, user_perms=[], *args, **kwargs +) -> bool: + """ + Edit stage accessibility + """ + return ( + request.user.has_perm("recruitment.change_stage") + or recruitment_manages(request.user, instance.recruitment_id) + or request.user.employee_get in instance.stage_managers.all + ) + + +def delete_stage_accessibility( + request, instance=None, user_perms=[], *args, **kwargs +) -> bool: + """ + Delete stage accessibility + """ + return request.user.has_perm("recruitment.delete_stage") diff --git a/recruitment/cbv/accessibility.py b/recruitment/cbv/accessibility.py new file mode 100644 index 000000000..5f8a453b6 --- /dev/null +++ b/recruitment/cbv/accessibility.py @@ -0,0 +1,173 @@ +""" +Accessibility +""" + +from django.contrib.auth.context_processors import PermWrapper +from django.contrib.auth.models import User +from base.methods import check_manager +from employee.models import Employee +from recruitment.methods import ( + in_all_managers, + is_recruitmentmanager, + is_stagemanager, + stage_manages, +) +from recruitment.models import Candidate, RecruitmentGeneralSetting, RejectedCandidate + + +def convert_emp(request, instance, user_perm): + """ + Covert employee accessibility + """ + mails = list(Candidate.objects.values_list("email", flat=True)) + existing_emails = list( + User.objects.filter(username__in=mails).values_list("email", flat=True) + ) + if not instance.email in existing_emails and not instance.start_onboard: + return True + + +def add_skill_zone(request, instance, user_perm): + """ + Add skill zone accessibility + """ + + mails = list(Candidate.objects.values_list("email", flat=True)) + existing_emails = list( + User.objects.filter(username__in=mails).values_list("email", flat=True) + ) + if not instance.email in existing_emails and request.user.has_perm( + "recruitment.add_skillzonecandidate" + ): + return True + + +def add_reject(request, instance, user_perm): + """ + add reject accessibility + """ + first = RejectedCandidate.objects.filter(candidate_id=instance).first() + mails = list(Candidate.objects.values_list("email", flat=True)) + existing_emails = list( + User.objects.filter(username__in=mails).values_list("email", flat=True) + ) + if not instance.email in existing_emails: + if request.user.has_perm( + "recruitment.add_rejectedcandidate" + ) or is_stagemanager(request): + if not first: + return True + + +def edit_reject(request, instance, user_perm): + """ + Edit reject accessibility + """ + first = RejectedCandidate.objects.filter(candidate_id=instance).first() + mails = list(Candidate.objects.values_list("email", flat=True)) + existing_emails = list( + User.objects.filter(username__in=mails).values_list("email", flat=True) + ) + if not instance.email in existing_emails: + if request.user.has_perm( + "recruitment.add_rejectedcandidate" + ) or is_stagemanager(request): + if first: + return True + + +def archive_status(request, instance, user_perm): + """ + To acces archive in list candidates + """ + if instance.is_active: + return True + + +def unarchive_status(request, instance, user_perm): + """ + To acces un-archive in list candidates + """ + if not instance.is_active: + return True + + +def onboarding_accessibility( + request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + """ + accessibility for onboarding tab in candidate individual view + """ + candidate = Candidate.objects.get(pk=instance.pk) + if candidate.cand_onboarding_task.exists() and in_all_managers(request) or request.user.has_perm("onboarding.view_onboardingtask"): + return True + return False + + +def rating_accessibility( + request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + """ + accessebility for rating tab in candidate individual view + """ + candidate = Candidate.objects.get(pk=instance.pk) + stage_manage = stage_manages(request.user.employee_get, candidate.recruitment_id) + if stage_manage or request.user.has_perm("recruitment.view_candidate") or request.user.has_perm("recruitment.view_candidate"): + return True + return False + + +def if_manager_accessibility(request, instance, *args, **kwargs): + """ + If manager accessibility + """ + return is_recruitmentmanager(request) or is_stagemanager(request) or request.user.has_perm("recruitment.view_candidate") + + +def empl_scheduled_interview_accessibility( + request, instance: object = None, user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + """ + sheduled interview tab accessibility for candidate individual view, employee individual view and employee profile + """ + employee = Employee.objects.get(id=instance.pk) + if ( + request.user.has_perm("recruitment.view_interviewschedule") + or check_manager(request.user.employee_get, instance) + or request.user == employee.employee_user_id + or is_recruitmentmanager(request) + ): + return True + return False + + +def view_candidate_self_tracking(request, instance, *args, **kwargs): + if ( + request.user.has_perm("recruitment.view_candidate") + or is_stagemanager(request) + or is_recruitmentmanager(request) + ): + return True + + +def request_document(request, instance, *args, **kwargs): + if ( + request.user.has_perm("recruitment.change_candidate") + or request.user.has_perm("recruitment.add_candidatedocumentrequest") + or is_stagemanager(request) + or is_recruitmentmanager(request) + ): + return True + + +def check_candidate_self_tracking(request, instance, user_perm): + """ + This method is used to get the candidate self tracking is enabled or not + """ + + candidate_self_tracking = False + if RecruitmentGeneralSetting.objects.exists(): + candidate_self_tracking = ( + RecruitmentGeneralSetting.objects.first().candidate_self_tracking + ) + return candidate_self_tracking diff --git a/recruitment/cbv/candidate_mail_log.py b/recruitment/cbv/candidate_mail_log.py new file mode 100644 index 000000000..00fc73cd5 --- /dev/null +++ b/recruitment/cbv/candidate_mail_log.py @@ -0,0 +1,54 @@ +""" +This page is handling the cbv methods of mail log tab in employee individual page. +""" + +from typing import Any +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse +from django.utils.decorators import method_decorator +from base.cbv.mail_log_tab import MailLogTabList +from horilla_views.generic.cbv.views import HorillaListView +from horilla_views.cbv_methods import login_required +from recruitment.models import Candidate +from recruitment.cbv_decorators import all_manager_can_enter + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + all_manager_can_enter(perm="recruitment.view_candidate"), name="dispatch" +) +class CandidateMailLogTabList(MailLogTabList): + """ + list view for mail log tab in candidate + """ + + def get_context_data(self, **kwargs: Any): + """ + Adds a search URL to the context based on the candidate's primary key. + """ + context = super().get_context_data(**kwargs) + pk = self.kwargs.get("pk") + context["search_url"] = ( + f"{reverse('candidate-mail-log-list',kwargs={'pk': pk})}" + ) + return context + + def get_queryset(self): + """ + Returns a filtered and ordered queryset of email logs for the specified candidate. + """ + + # queryset = super().get_queryset() + pk = self.kwargs.get("pk") + candidate_obj = Candidate.objects.get(id=pk) + return ( + HorillaListView.get_queryset(self) + .filter(to__icontains=candidate_obj.email) + .order_by("-created_at") + ) + + def dispatch(self, request, *args, **kwargs): + """ + To avoide parent permissions + """ + return super(MailLogTabList, self).dispatch(request, *args, **kwargs) diff --git a/recruitment/cbv/candidate_profile.py b/recruitment/cbv/candidate_profile.py new file mode 100644 index 000000000..5c712f157 --- /dev/null +++ b/recruitment/cbv/candidate_profile.py @@ -0,0 +1,176 @@ +""" +This page handles the cbv methods for canidate profile page +""" + +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from employee.cbv.employee_profile import EmployeeProfileView +from horilla import settings +from horilla_views.generic.cbv.views import HorillaProfileView, HorillaListView +from recruitment.cbv.candidate_mail_log import CandidateMailLogTabList +from recruitment.filters import CandidateFilter +from recruitment.views import views +from recruitment.models import Candidate +from recruitment.cbv_decorators import all_manager_can_enter +from horilla_views.cbv_methods import login_required +from onboarding.models import CandidateTask +from onboarding.filters import CandidateTaskFilter + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + all_manager_can_enter(perm="recruitment.view_candidate"), name="dispatch" +) +class CandidateProfileView(HorillaProfileView): + """ + Candidate ProfileView + """ + + model = Candidate + filter_class = CandidateFilter + push_url = "candidate-view-individual" + key_name = "cand_id" + + actions = [ + { + "title": _("Edit"), + "src": f"/{settings.STATIC_URL}images/ui/edit_btn.png", + "attrs": """ + onclick=" + window.location.href='{get_update_url}' " + """, + }, + { + "title": _("View candidate self tracking"), + "src": f"/{settings.STATIC_URL}images/ui/exit-outline.svg", + "accessibility": "recruitment.cbv.accessibility.view_candidate_self_tracking", + "attrs": """ + href="{get_self_tracking_url}" + class="oh-dropdown__link" + """, + }, + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + all_manager_can_enter(perm="recruitment.view_candidate"), name="dispatch" +) +class CandidateProfileTasks(HorillaListView): + """ + CandidateProfileTasks + """ + + custom_empty_template = "onboarding/empty_task.html" + model = CandidateTask + show_filter_tags = False + filter_class = CandidateTaskFilter + filter_selected = False + selected_instances_key_id = "selectedInstanceIds" + bulk_update_fields = [ + "status", + ] + + def bulk_update_accessibility(self): + return ( + super().bulk_update_accessibility() + or self.request.user.employee_get.onboardingstage_set.filter( + candidate__candidate_id__pk=self.kwargs["pk"] + ).exists() + ) + + columns = [ + ("Task", "onboarding_task_id__task_title"), + ("Status", "status_col"), + ( + "Modified By", + "modified_by__employee_get__get_full_name", + "modified_by__employee_get__get_avatar", + ), + ] + + header_attrs = { + "status_col": """ + style="width:180px!important;" +""" + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.search_url = self.request.path + + def get_queryset(self, queryset=None, filtered=False, *args, **kwargs): + self.queryset = ( + super() + .get_queryset(queryset, filtered, *args, **kwargs) + .filter(candidate_id__pk=self.kwargs["pk"]) + ) + return self.queryset + + +CandidateProfileView.add_tab( + tabs=[ + { + "title": "About", + "view": views.candidate_about_tab, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Resume", + "view": views.candidate_resume_tab, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Survey", + "view": views.candidate_survey_tab, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Documents", + "view": views.candidate_document_request_tab, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Notes", + "view": views.add_note, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "History", + "view": views.candidate_history_tab, + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Rating", + "view": views.candidate_rating_tab, + "accessibility": "recruitment.cbv.accessibility.rating_accessibility", + }, + { + "title": "Onboarding", + "view": CandidateProfileTasks.as_view(), + "accessibility": "recruitment.cbv.accessibility.onboarding_accessibility", + }, + { + "title": "Mail Log", + # "view": views.get_mail_log + "view": CandidateMailLogTabList.as_view(), + "accessibility": "recruitment.cbv.accessibility.if_manager_accessibility", + }, + { + "title": "Sheduled Interviews", + "view": views.candidate_interview_tab, + "accessibility": "recruitment.cbv.accessibility.empl_scheduled_interview_accessibility", + }, + ] +) + + +EmployeeProfileView.add_tab( + tabs=[ + { + "title": "Scheduled Interviews", + "view": views.scheduled_interview_tab, + "accessibility": "recruitment.cbv.accessibility.empl_scheduled_interview_accessibility", + }, + ] +) diff --git a/recruitment/cbv/candidate_reject_reason.py b/recruitment/cbv/candidate_reject_reason.py new file mode 100644 index 000000000..ed455a506 --- /dev/null +++ b/recruitment/cbv/candidate_reject_reason.py @@ -0,0 +1,159 @@ +""" +This page handles reject reason in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from django.contrib import messages +from recruitment.filters import RejectReasonFilter +from horilla_views.cbv_methods import permission_required, login_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) +from recruitment.forms import RejectReasonForm +from recruitment.models import RejectReason + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_rejectreason"), name="dispatch" +) +class RejectReasonListView(HorillaListView): + """ + List view of the rejected reason page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + # self.action_method = "actions_col" + self.view_id = "reje_reason" + self.search_url = reverse("candidate-reject-reasons-list") + + model = RejectReason + filter_class = RejectReasonFilter + + columns = [ + (_("Reject Reasons"), "title"), + (_("Description"), "description"), + ] + + actions = [ + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-50" + hx-get="{get_update_url}?instance_ids={ordered_ids}" + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + }, + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + id = "delete-reject" + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-50" + hx-confirm="Are you sure want to delete this reason?" + hx-target="#rejectReasonTr{get_instance_id}" + hx-post="{get_delete_url}" + hx-swap="delete" + """, + }, + ] + + header_attrs = { + "title": """ style="width:200px !important" """, + "description": """ style="width:200px !important" """, + "action": """ style="width:200px !important" """ + } + + row_attrs = """ + id = "rejectReasonTr{get_instance_id}" + """ + +# onclick="deleteRejectReason('{get_delete_url}')" +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_rejectreason"), name="dispatch" +) +class RejectReasonNav(HorillaNavView): + """ + Nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("candidate-reject-reasons-list") + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('create-reject-reason-view')}" + """ + + nav_title = _("Reject Reasons") + filter_instance = RejectReasonFilter() + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.add_rejectreason"), name="dispatch" +) +class RejectReasonFormView(HorillaFormView): + """ + Create and edit form + """ + + model = RejectReason + form_class = RejectReasonForm + new_display_title = _("Create reject reason") + + def get_context_data(self, **kwargs): + """ + Get context data for rendering the form view. + """ + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update reject reason") + return context + + def form_valid(self, form: RejectReasonForm) -> HttpResponse: + """ + Handle a valid form submission. + + If the form is valid, save the instance and display a success message. + """ + if form.is_valid(): + if form.instance.pk: + message = _("Reject reason updated successfully.") + else: + message = _("Reject reason created successfully.") + form.save() + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) + +class DynamicRejectReasonFormView(HorillaFormView): + + model = RejectReason + form_class = RejectReasonForm + new_display_title = _("Create reject reason") + is_dynamic_create_view = True + + def form_valid(self, form: RejectReasonForm) -> HttpResponse: + + if form.is_valid(): + message = _("Reject reason created successfully.") + messages.success(self.request, _(message)) + form.save() + return self.HttpResponse() + return super().form_valid(form) diff --git a/recruitment/cbv/candidates.py b/recruitment/cbv/candidates.py new file mode 100644 index 000000000..92460c6a3 --- /dev/null +++ b/recruitment/cbv/candidates.py @@ -0,0 +1,1003 @@ +""" +This module used for recruitment candidates +""" + +import io +import ast +import json +import re +from bs4 import BeautifulSoup +from xhtml2pdf import pisa +from openpyxl import Workbook +from openpyxl.styles import PatternFill, Font, Border, Side, Alignment +from openpyxl.utils import get_column_letter +from typing import Any +from import_export import fields, resources +from django.http import HttpResponse +from django.shortcuts import render +from django.utils.decorators import method_decorator +from django.urls import reverse, reverse_lazy +from django.views import View +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from django import forms +from django.template.loader import render_to_string +from horilla.horilla_middlewares import _thread_locals +from employee.forms import BulkUpdateFieldForm +from recruitment.cbv.candidate_reject_reason import DynamicRejectReasonFormView +from recruitment.filters import CandidateFilter +from recruitment.forms import ( + CandidateExportForm, + RejectedCandidateForm, + ToSkillZoneForm, +) +from recruitment.models import ( + Candidate, + RejectedCandidate, + SkillZoneCandidate, + RecruitmentSurvey, + RecruitmentSurveyAnswer, +) +from recruitment.cbv_decorators import manager_can_enter, all_manager_can_enter +from horilla_views.forms import DynamicBulkUpdateForm +from horilla_views.templatetags.generic_template_filters import getattribute +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + TemplateView, + HorillaNavView, + HorillaCardView, + HorillaDetailedView, +) +from horilla_views.cbv_methods import export_xlsx, login_required, permission_required + +_getattribute = getattribute + + +def clean_column_name(question): + """ + Convert the question text into a safe attribute name by: + - Replacing spaces with underscores + - Removing special characters except underscores + """ + return re.sub(r"[^\w\s]", "", question).replace(" ", "_") + + +@method_decorator( + permission_required(perm="recruitment.view_candidate"), name="dispatch" +) +@method_decorator(login_required, name="dispatch") +class CandidatesView(TemplateView): + """ + For page view + + """ + + template_name = "cbv/candidates/candidates.html" + + def get_context_data(self, **kwargs: Any) -> dict: + context = super().get_context_data(**kwargs) + update_fields = BulkUpdateFieldForm() + context["update_fields_form"] = update_fields + return context + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch") +class ListCandidates(HorillaListView): + """ + List view of candidates + """ + + model = Candidate + filter_class = CandidateFilter + bulk_template = "cbv/employees_view/bulk_update_page.html" + bulk_update_fields = [ + "gender", + "job_position_id", + "hired_date", + "referral", + "country", + "state", + "city", + "zip", + "joining_date", + "probation_end", + ] + + def get_bulk_form(self): + """ + Bulk from generating method + """ + + form = DynamicBulkUpdateForm( + root_model=Candidate, bulk_update_fields=self.bulk_update_fields + ) + + form.fields["country"] = forms.ChoiceField( + required=False, + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2", + "required": False, + "style": "width: 100%; height:45px;", + } + ), + ) + + form.fields["state"] = forms.ChoiceField( + required=False, + widget=forms.Select( + attrs={ + "class": "oh-select oh-select-2", + "required": False, + "style": "width: 100%; height:45px;", + }, + ), + ) + + return form + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.export_fields = [] + self.search_url = reverse("list-candidate") + if self.request.user.has_perm("recruitment.change_candidate"): + self.option_method = "options" + else: + self.option_method = None + + unique_questions = RecruitmentSurvey.objects.values_list( + "question", flat=True + ).distinct() + self.survey_question_mapping = {} + for question in unique_questions: + survey_question = (question, f"question_{clean_column_name(question)}") + self.survey_question_mapping[f"question_{clean_column_name(question)}"] = ( + question + ) + if not survey_question in self.export_fields: + self.export_fields.append(survey_question) + + columns = [ + ("Candidates", "name", "get_avatar"), + ("Email", "email"), + ("Phone", "mobile"), + ("Rating", "rating"), + ("Recruitment", "recruitment_id"), + ("Job Position", "job_position_id"), + ("Hired Date", "hired_date"), + ("Resume", "resume_pdf"), + ] + default_columns = columns + + header_attrs = { + "option": """ + style ="width : 230px !important;" + """, + "action": """ + style ="width : 200px !important;" + """, + "email": """ + style ="width : 200px !important;" + """, + } + + actions = [ + { + "action": "Edit", + "icon": "create-outline", + "attrs": """class="oh-btn oh-btn--light-bkg w-100" + onclick="event.stopPropagation() + window.location.href='{get_update_url}' " + """, + }, + { + "action": "Archive", + "accessibility": "recruitment.cbv.accessibility.archive_status", + "icon": "archive", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + onclick="event.stopPropagation() + archiveCandidate({get_archive_url}); " + """, + }, + { + "action": "Un-archive", + "accessibility": "recruitment.cbv.accessibility.unarchive_status", + "icon": "archive", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + onclick="event.stopPropagation() + archiveCandidate({get_archive_url}); " + """, + }, + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get="{get_delete_url}?model=recruitment.candidate&pk={pk}" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-target="#deleteConfirmationBody" + """, + }, + ] + + sortby_mapping = [ + ("Candidates", "name", "get_avatar"), + ] + row_status_indications = [ + ( + "canceled--dot", + "Canceled", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=canceled]').val('true'); + $('#applyFilter').click(); + " + """, + ), + ( + "nothired--dot", + "Not Hired", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=hired]').val('false'); + $('#applyFilter').click(); + " + """, + ), + ( + "hired--dot", + "Hired", + """ + onclick="$('#applyFilter').closest('form').find('[name=hired]').val('true'); + $('#applyFilter').click(); + " + """, + ), + ] + + records_per_page = 10 + + row_status_class = "hired-{hired} canceled-{canceled}" + + # row_attrs = """ + # {is_employee_converted} + # hx-get='{get_details_candidate}' + # data-toggle="oh-modal-toggle" + # data-target="#genericModal" + # hx-target="#genericModalBody" + # """ + row_attrs = """ + {is_employee_converted} + onclick="window.location.href='{get_individual_url}?instance_ids={ordered_ids}'" + """ + + def export_data(self, *args, **kwargs): + """ + Export with survey answer and question + """ + + request = getattr(_thread_locals, "request", None) + ids = ast.literal_eval(request.POST["ids"]) + _columns = ast.literal_eval(request.POST["columns"]) + queryset = self.model.objects.filter(id__in=ids) + question_mapping = self.survey_question_mapping + export_format = request.POST.get("format", "xlsx") + + _model = self.model + + class HorillaListViewResorce(resources.ModelResource): + """ + Instant Resource class + """ + + id = fields.Field(column_name="ID") + question = {} + + class Meta: + """ + Meta class for additional option + """ + + model = _model + fields = [] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + for field_tuple in _columns: + if field_tuple[1].startswith("question_"): + safe_field_name = field_tuple[1] + self.fields[safe_field_name] = fields.Field( + column_name=question_mapping[safe_field_name], + attribute=safe_field_name, + readonly=True, + ) + + def export_field(self, field, obj): + """ + Override this method to fetch the candidate's answers dynamically. + """ + + if field.attribute: + # Get the stored JSON field containing answers + survey_answers = RecruitmentSurveyAnswer.objects.filter( + candidate_id=obj + ).first() + if survey_answers and field.attribute.startswith("question_"): + survey_answers = survey_answers.answer_json + if isinstance(survey_answers, str): + try: + survey_answers = ast.literal_eval( + survey_answers + ) # Convert string to dict + except Exception: + survey_answers = {} + + # Extract the actual question text + + original_question = question_mapping[field.attribute] + # Retrieve answer from JSON if available + answer = survey_answers.get(original_question, "") + if not answer: + answer = survey_answers.get( + "rating_" + original_question, "" + ) + if not answer: + answer = survey_answers.get( + "percentage_" + original_question, "" + ) + if not answer: + answer = survey_answers.get("file_" + original_question, "") + if not answer: + answer = survey_answers.get("date_" + original_question, "") + if not answer: + answer = survey_answers.get( + "multiple_choices_" + original_question, "" + ) + return answer + + return super().export_field(field, obj) + + def dehydrate_id(self, instance): + """ + Dehydrate method for id field + """ + return instance.pk + + for field_tuple in _columns: + if not field_tuple[1].startswith("question_"): + dynamic_fn_str = f"def dehydrate_{field_tuple[1]}(self, instance):return self.remove_extra_spaces(getattribute(instance, '{field_tuple[1]}'))" + exec(dynamic_fn_str) + dynamic_fn = locals()[f"dehydrate_{field_tuple[1]}"] + locals()[field_tuple[1]] = fields.Field(column_name=field_tuple[0]) + + def remove_extra_spaces(self, text): + """ + Remove blank space but keep line breaks and add new lines for
  • tags. + """ + soup = BeautifulSoup(str(text), "html.parser") + for li in soup.find_all("li"): + li.insert_before("\n") + li.unwrap() + text = soup.get_text() + lines = text.splitlines() + non_blank_lines = [line.strip() for line in lines if line.strip()] + cleaned_text = "\n".join(non_blank_lines) + return cleaned_text + + book_resource = HorillaListViewResorce() + + # Export the data using the resource + dataset = book_resource.export(queryset) + + # Set the response headers + # file_name = self.export_file_name + if export_format == "json": + json_data = json.loads(dataset.export("json")) + response = HttpResponse( + json.dumps(json_data, indent=4), content_type="application/json" + ) + response["Content-Disposition"] = ( + f'attachment; filename="{self.export_file_name}.json"' + ) + return response + + # CSV + elif export_format == "csv": + csv_data = dataset.export("csv") + response = HttpResponse(csv_data, content_type="text/csv") + response["Content-Disposition"] = ( + f'attachment; filename="{self.export_file_name}.csv"' + ) + return response + elif export_format == "pdf": + + headers = dataset.headers + rows = dataset.dict + + # Render to HTML using a template + html_string = render_to_string( + "generic/export_pdf.html", + { + "headers": headers, + "rows": rows, + }, + ) + + # Convert HTML to PDF using xhtml2pdf + result = io.BytesIO() + pisa_status = pisa.CreatePDF(html_string, dest=result) + + if pisa_status.err: + return HttpResponse("PDF generation failed", status=500) + + # Return response + response = HttpResponse(result.getvalue(), content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="{self.export_file_name}.pdf"' + ) + return response + + # response = HttpResponse( + # dataset.export("xlsx"), content_type="application/vnd.ms-excel" + # ) + # response["Content-Disposition"] = ( + # f'attachment; filename="{self.export_file_name}.xls"' + # ) + json_data = json.loads(dataset.export("json")) + headers = list(json_data[0].keys()) if json_data else [] + + wb = Workbook() + ws = wb.active + ws.title = "Exported Data" + + # Styling + header_fill = PatternFill(start_color="FFD700", end_color="FFD700", fill_type="solid") + bold_font = Font(bold=True) + thin_border = Border( + left=Side(style="thin"), + right=Side(style="thin"), + top=Side(style="thin"), + bottom=Side(style="thin"), + ) + wrap_alignment = Alignment(vertical="top", wrap_text=True) + + # Write headers + for col_idx, header in enumerate(headers, start=1): + cell = ws.cell(row=1, column=col_idx, value=header) + cell.fill = header_fill + cell.font = bold_font + cell.border = thin_border + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + + # Write data rows + for row_idx, item in enumerate(json_data, start=2): + for col_idx, key in enumerate(headers, start=1): + value = item.get(key, "") + # Convert lists to newline-separated string + if isinstance(value, list): + value = "\n".join(str(v) for v in value) + elif isinstance(value, dict): + value = json.dumps(value, ensure_ascii=False) # or format it as needed + cell = ws.cell(row=row_idx, column=col_idx, value=value) + cell.border = thin_border + cell.alignment = wrap_alignment + + # Auto-fit column widths + for col_cells in ws.columns: + max_len = max(len(str(cell.value or "")) for cell in col_cells) + col_letter = get_column_letter(col_cells[0].column) + ws.column_dimensions[col_letter].width = min(max_len + 5, 50) + + # Output to Excel + output = io.BytesIO() + wb.save(output) + output.seek(0) + + response = HttpResponse( + output.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + response["Content-Disposition"] = 'attachment; filename="exported_data.xlsx"' + return response + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch") +class CardCandidates(HorillaCardView): + """ + For card view + """ + + model = Candidate + filter_class = CandidateFilter + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("card-candidate") + + details = { + "image_src": "get_avatar", + "title": "{get_full_name}", + "subtitle": "{email}
    {get_job_position}", + } + + actions = [ + { + "action": "Convert to Employee", + "accessibility": "recruitment.cbv.accessibility.convert_emp", + "attrs": """ + onclick="event.stopPropagation() + return confirm('Are you sure you want to convert this candidate into an employee?')" + href='{get_convert_to_emp}' + class="oh-dropdown__link" + + """, + }, + { + "action": "Add to Skill Zone", + "accessibility": "recruitment.cbv.accessibility.add_skill_zone", + "attrs": """ + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{get_add_to_skill}" + hx-target="#genericModalBody" + class="oh-dropdown__link" + + """, + }, + { + "action": "View candidate self tracking", + "accessibility": "recruitment.cbv.accessibility.check_candidate_self_tracking", + "attrs": """ + href="{get_self_tracking_url}" + class="oh-dropdown__link" + """, + }, + { + "action": "Request Document", + "accessibility": "recruitment.cbv.accessibility.request_document", + "attrs": """ + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{get_document_request_doc}" + hx-target="#genericModalBody" + class="oh-dropdown__link" + """, + }, + { + "action": "Add to Rejected", + "accessibility": "recruitment.cbv.accessibility.add_reject", + "attrs": """ + hx-target="#genericModalBody" + hx-swap="innerHTML" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{get_add_to_reject}" + class="oh-dropdown__link" + + """, + }, + { + "action": "Edit Rejected Candidate", + "accessibility": "recruitment.cbv.accessibility.edit_reject", + "attrs": """ + hx-target="#genericModalBody" + hx-swap="innerHTML" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{get_add_to_reject}" + class="oh-dropdown__link" + + """, + }, + { + "action": "Edit Profile", + "attrs": """ + onclick="event.stopPropagation() + window.location.href='{get_update_url}' " + class="oh-dropdown__link" + + """, + }, + { + "action": "archive_status", + "attrs": """ + class="oh-dropdown__link" + onclick="archiveCandidate({get_archive_url});" + + + """, + }, + { + "action": "Delete", + "attrs": """ + class="oh-dropdown__link oh-dropdown__link--danger" + onclick="event.stopPropagation(); + deleteCandidate('{get_delete_url}'); " + + """, + }, + ] + card_status_indications = [ + ( + "canceled--dot", + "Canceled", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=canceled]').val('true'); + $('#applyFilter').click(); + " + """, + ), + ( + "nothired--dot", + "Not Hired", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=hired]').val('false'); + $('#applyFilter').click(); + " + """, + ), + ( + "hired--dot", + "Hired", + """ + onclick="$('#applyFilter').closest('form').find('[name=hired]').val('true'); + $('#applyFilter').click(); + " + """, + ), + ] + card_status_class = "hired-{hired} canceled-{canceled}" + card_attrs = """ + onclick="window.location.href='{get_individual_url}?instance_ids={ordered_ids}'" + """ + + records_per_page = 30 + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch") +class CandidateNav(HorillaNavView): + """ + For nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-candidate") + self.create_attrs = f""" + href='{reverse_lazy('candidate-create')}'" + """ + self.actions = [ + { + "action": "Export", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('export')}" + hx-target="#genericModalBody" + """, + }, + { + "action": "Bulk mail", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('send-mail')}" + hx-target="#genericModalBody" + """, + }, + { + "action": "Create document request", + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#objectCreateModal" + hx-get="{reverse('candidate-document-request')}" + hx-target="#objectCreateModalTarget" + """, + }, + { + "action": "Archive", + "attrs": """ + id="archiveCandidates" + + """, + }, + { + "action": "Un archive", + "attrs": """ + id="unArchiveCandidates" + + """, + }, + { + "action": "Delete", + "attrs": """ + data-action = "delete" + id="deleteCandidates" + new_init + """, + }, + ] + + self.view_types = [ + { + "type": "list", + "icon": "list-outline", + "url": reverse("list-candidate"), + "attrs": """ + title='List' + """, + }, + { + "type": "card", + "icon": "grid-outline", + "url": reverse("card-candidate"), + "attrs": """ + title='Card' + """, + }, + ] + + nav_title = "Candidates" + filter_body_template = "cbv/candidates/filter.html" + filter_instance = CandidateFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + group_by_fields = [ + ("recruitment_id", "Recruitment"), + ("job_position_id", "Job Position"), + ("hired", "Hired"), + ("country", "Country"), + ("stage_id", "Stage"), + ("joining_date", "Date joining"), + ("probation_end", "Probation End"), + ("offer_letter_status", "offer Letter Status"), + ("rejected_candidate__reject_reason_id", "Reject reason"), + ("skillzonecandidate_set", "Skill zone"), + ] + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch") +class ExportView(TemplateView): + """ + For candidate export + """ + + template_name = "cbv/candidates/export.html" + + def get_context_data(self, **kwargs: Any): + """ + Adds export fields and filter object to the context. + """ + context = super().get_context_data(**kwargs) + candidates = Candidate.objects.filter(is_active=True) + export_column = CandidateExportForm() + export_filter = CandidateFilter(queryset=candidates) + context["export_column"] = export_column + context["export_filter"] = export_filter + return context + + +@method_decorator(login_required, name="dispatch") +@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch") +class AddToRejectedCandidatesView(View): + """ + Class for Add to reject candidate + """ + + template_name = "onboarding/rejection/form.html" + + def get(self, request): + """ + get method + """ + candidate_id = request.GET.get("candidate_id") + instance = None + if candidate_id: + instance = RejectedCandidate.objects.filter( + candidate_id=candidate_id + ).first() + form = RejectedCandidateForm( + initial={"candidate_id": candidate_id}, instance=instance + ) + return render(request, self.template_name, {"form": form}) + + def post(self, request): + """ + post method + """ + candidate_id = request.GET.get("candidate_id") + instance = None + if candidate_id: + instance = RejectedCandidate.objects.filter( + candidate_id=candidate_id + ).first() + form = RejectedCandidateForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Candidate reject reason saved") + return HttpResponse("") + return render(request, self.template_name, {"form": form}) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + all_manager_can_enter(perm="recruitment.view_candidate"), name="dispatch" +) +class CandidateDetail(HorillaDetailedView): + """ + Candidate detail + """ + + title = "Candidate Details" + + model = Candidate + + header = {"title": "get_full_name", "subtitle": "get_email", "avatar": "get_avatar"} + + body = [ + ("Gender", "gender"), + ("Phone", "mobile"), + ("Stage", "stage_drop_down"), + ("Rating", "rating_bar"), + ("Recruitment", "recruitment_id"), + ("Job Position", "job_position_id"), + ("Interview Table", "candidate_interview_view", True), + ] + + cols = { + "candidate_interview_view": 12, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.actions = [ + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--info w-50" + onclick="window.location.href='{get_update_url}' " + """, + }, + { + "action": "View", + "icon": "eye-outline", + "attrs": """ + class="oh-btn oh-btn--success w-50" + onclick="window.location.href='{get_individual_url}'" + """, + }, + ] + + if self.request.user.has_perm("recruitment.delete_candidate"): + self.actions.append( + { + "action": "Delete", + "icon": "trash-outline", + "accessibility": "recruitment.cbv.candidates.delete_cand", + "attrs": f""" + class="oh-btn oh-btn--danger w-50" + hx-get="{reverse_lazy("generic-delete")}?model=recruitment.Candidate&pk={{pk}}" + hx-target="#deleteConfirmationBody" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + onclick="event.stopPropagation() + deleteCandidate('{{get_delete_url}}'); " + """, + } + ) + + +class ToSkillZoneFormView(HorillaFormView): + """ + Form View + """ + + model = SkillZoneCandidate + form_class = ToSkillZoneForm + new_display_title = "Add To Skill Zone" + + def get_context_data(self, **kwargs): + """ + Returns context with form and candidate data. + """ + context = super().get_context_data(**kwargs) + candidate_id = self.kwargs.get("cand_id") + candidate = Candidate.objects.get(id=candidate_id) + form = self.form_class( + initial={ + "candidate_id": candidate, + "skill_zone_ids": SkillZoneCandidate.objects.filter( + candidate_id=candidate + ).values_list("skill_zone_id", flat=True), + } + ) + context["form"] = form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + """ + Handles and renders form errors or defers to superclass. + """ + form = self.form_class(self.request.POST) + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: ToSkillZoneForm) -> HttpResponse: + """ + Handles valid form submission and saves rejected candidate reason. + """ + if form.is_valid(): + candidate_id = self.kwargs.get("cand_id") + candidate = Candidate.objects.get(id=candidate_id) + self.form_class( + initial={ + "candidate_id": candidate, + "skill_zone_ids": SkillZoneCandidate.objects.filter( + candidate_id=candidate + ).values_list("skill_zone_id", flat=True), + } + ) + skill_zones = self.form.cleaned_data["skill_zone_ids"] + for zone in skill_zones: + if not SkillZoneCandidate.objects.filter( + candidate_id=candidate_id, skill_zone_id=zone + ).exists(): + zone_candidate = SkillZoneCandidate() + zone_candidate.candidate_id = candidate + zone_candidate.skill_zone_id = zone + zone_candidate.reason = self.form.cleaned_data["reason"] + zone_candidate.save() + message = "Candidate Added to skill zone successfully" + messages.success(self.request, _(message)) + return self.HttpResponse() + return super().form_valid(form) + + +class RejectReasonFormView(HorillaFormView): + """ + Form View + """ + + model = RejectedCandidate + form_class = RejectedCandidateForm + new_display_title = "Rejected Candidate" + dynamic_create_fields = [("reject_reason_id", DynamicRejectReasonFormView)] + + def get_initial(self) -> dict: + initial = super().get_initial() + initial["candidate_id"] = self.request.GET.get("candidate_id") + return initial + + def init_form(self, *args, data={}, files={}, instance=None, **kwargs): + candidate_id = self.request.GET.get("candidate_id") + instance = RejectedCandidate.objects.filter(candidate_id=candidate_id).first() + return super().init_form( + *args, data=data, files=files, instance=instance, **kwargs + ) + + def form_valid(self, form: RejectedCandidateForm) -> HttpResponse: + """ + Handles valid form submission and saves rejected candidate reason. + """ + if form.is_valid(): + message = _("Candidate reject reason saved") + messages.success(self.request, _(message)) + form.save() + return self.HttpResponse() + return super().form_valid(form) diff --git a/recruitment/cbv/dashboard.py b/recruitment/cbv/dashboard.py new file mode 100644 index 000000000..108ff6e3c --- /dev/null +++ b/recruitment/cbv/dashboard.py @@ -0,0 +1,191 @@ +""" +Dashboard of recruitment +""" + +from typing import Any +from django.urls import reverse +from django.db.models import Count, Q +from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator +from base.filters import JobRoleFilter +from base.models import JobPosition +from horilla_views.generic.cbv.views import HorillaListView +from horilla_views.cbv_methods import login_required +from recruitment.cbv_decorators import manager_can_enter +from recruitment.filters import CandidateFilter, RecruitmentFilter, SkillZoneFilter +from recruitment.models import Candidate, Recruitment, SkillZone + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class SkillZoneStatusList(HorillaListView): + """ + List view for skill zone status in recruitment dashboard + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("skill-zone-status-dashboard") + + model = SkillZone + filter_class = SkillZoneFilter + columns = [ + (_("Skill Zone"), "title", "get_avatar"), + (_("No. of Candidates"), "candidate_count_display"), + ] + bulk_select_option = False + + header_attrs = { + "title": """ + style="width:150px !important" + """ + } + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(is_active=True) + return queryset + + row_attrs = """ + onclick = "window.location.href='{get_skill_zone_url}'" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class CandidateOnOnboardList(HorillaListView): + """ + List view for candidate on onboard in recruitment dashboard + """ + + model = Candidate + filter_class = CandidateFilter + bulk_select_option = False + + columns = [ + (_("Candidates"), "name", "get_avatar"), + (_("Job Position"), "job_position_id"), + ] + + header_attrs = { + "name": """ + style="width:100px !important" + """ + } + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(onboarding_stage__isnull=False) + return queryset + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("candidate-on-onboard-dashboard") + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class CurrentHiringList(HorillaListView): + """ + List view for hiring in each job position in dashboard + """ + + model = JobPosition + filter_class = JobRoleFilter + bulk_select_option = False + records_per_page = 10 + + columns = [ + (_("Job Positions"), "job_position"), + (_("Initial"), "initial_count"), + (_("Test"), "test_count"), + (_("Interview"), "interview_count"), + (_("Hired"), "hired_count"), + (_("Cancelled"), "cancelled_count"), + ] + + header_attrs = { + "job_position": """ + style = "width:100px !important " + """, + "initial_count": """ + style = "width:55px !important" + """, + "test_count": """ + style = "width:55px !important;" + """, + "interview_count": """ + style = "width:60px !important" + """, + "hired_count": """ + style = "width:55px !important" + """, + "cancelled_count": """ + style = "width:65px !important" + """, + } + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + initial_count=Count( + "candidate", filter=Q(candidate__stage_id__stage_type="initial") + ), + test_count=Count( + "candidate", filter=Q(candidate__stage_id__stage_type="test") + ), + interview_count=Count( + "candidate", filter=Q(candidate__stage_id__stage_type="interview") + ), + hired_count=Count( + "candidate", filter=Q(candidate__stage_id__stage_type="hired") + ), + cancelled_count=Count( + "candidate", filter=Q(candidate__stage_id__stage_type="cancelled") + ), + ) + return queryset + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("current-hiring-pipeline-dashboard") + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class OnGoingRecruitmentList(HorillaListView): + """ + List view for ongoing recruitment and its managers in dashboard + """ + + model = Recruitment + filter_class = RecruitmentFilter + bulk_select_option = False + + columns = [ + (_("Recrutment"), "title"), + (_("Managers"), "managers"), + ] + + header_attrs = { + "title": """ + style="width:100px !important" + """ + } + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(closed=False) + return queryset + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("ongoing-recruitment-dashboard") diff --git a/recruitment/cbv/interview.py b/recruitment/cbv/interview.py new file mode 100644 index 000000000..2840f5362 --- /dev/null +++ b/recruitment/cbv/interview.py @@ -0,0 +1,251 @@ +""" +this page handles cbv of interview page +""" + +from typing import Any + +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import resolve, reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ + +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) +from recruitment.decorators import recruitment_manager_can_enter, manager_can_enter +from notifications.signals import notify +from recruitment.filters import InterviewFilter +from recruitment.forms import ScheduleInterviewForm +from recruitment.models import Candidate, InterviewSchedule + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + recruitment_manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class InterviewViewPage(TemplateView): + """ + for interview page + """ + + template_name = "cbv/interview/interview_home_view.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + recruitment_manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class InterviewNavView(HorillaNavView): + """ + nav bar of the page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("interview-list-view") + + if self.request.user.has_perm("view_interviewschedule"): + self.create_attrs = f""" + hx-get="{reverse_lazy("create-interview-schedule")}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + else: + self.create_attrs = None + + nav_title = _("Scheduled Interviews") + filter_instance = InterviewFilter() + filter_body_template = "cbv/interview/interview_filter.html" + filter_form_context_name = "form" + search_swap_target = "#listContainer" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + recruitment_manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class InterviewLIstView(HorillaListView): + """ + list view of the page + """ + + bulk_update_fields = ["employee_id", "interview_date", "interview_time"] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("interview-list-view") + self.view_id = "interviewdelete" + + def get_queryset(self): + """ + Override queryset based on user permissions. + + Returns: + queryset: Filtered queryset based on user permissions and employee ID. + """ + queryset = super().get_queryset() + if self.request.user.has_perm("view_interviewschedule"): + queryset = queryset.all().order_by("-interview_date") + else: + queryset = queryset.filter( + employee_id=self.request.user.employee_get.id + ).order_by("-interview_date") + + return queryset + + filter_class = InterviewFilter + model = InterviewSchedule + records_per_page = 10 + template_name = "cbv/interview/inherit_script.html" + + columns = [ + (_("Candidate"), "candidate_custom_col"), + (_("Interviewer"), "interviewer_custom_col"), + (_("Interview Date"), "interview_date"), + (_("Interview Time"), "interview_time"), + (_("Description"), "get_description"), + (_("Status"), "status_custom_col"), + ] + header_attrs = { + "candidate_custom_col": """ + style="width:200px !important;" + """ + } + + sortby_mapping = [ + ("Interview Date", "interview_date"), + ("Interview Time", "interview_time"), + ] + action_method = "custom_action_col" + + row_attrs = """ + {custom_color} + class="oh-permission-table--collapsed" + hx-get='{detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + recruitment_manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class InterviewDetailView(HorillaDetailedView): + """ + detailed view + """ + + model = InterviewSchedule + title = _("Details") + header = { + "title": "candidate_id__get_full_name", + "subtitle": "detail_subtitle", + "avatar": "candidate_id__get_avatar", + } + body = [ + (_("Candidate"), "candidate_id"), + (_("Interviewer"), "interviewer_detail"), + (_("Interview Date"), "interview_date"), + (_("Interview Time"), "interview_time"), + (_("Description"), "get_description"), + (_("Status"), "status_custom_col"), + ] + action_method = "detail_view_actions" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.add_interviewschedule"), name="dispatch" +) +class InterviewForm(HorillaFormView): + """ + form view + """ + + form_class = ScheduleInterviewForm + model = InterviewSchedule + new_display_title = _("Schedule Interview") + + def get_context_data(self, **kwargs): + """ + Override to add custom context data. + """ + context = super().get_context_data(**kwargs) + resolved = resolve(self.request.path_info) + cand_id = resolved.kwargs.get("cand_id") + if cand_id: + self.form.fields["candidate_id"].queryset = Candidate.objects.filter( + id=cand_id + ) + self.form.fields["candidate_id"].initial = cand_id + if self.form.instance.pk: + self.form_class.verbose_name = _("Schedule Interview") + context["form"] = self.form + context["view_id"] = "InterviewCreate" + return context + + def form_invalid(self, form: Any) -> HttpResponse: + if self.form.instance.pk: + self.form_class.verbose_name = _("Schedule Interview") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: ScheduleInterviewForm) -> HttpResponse: + """ + Handle form submission when the form is valid. + + Args: + form (ScheduleInterviewForm): The validated form object. + + Returns: + HttpResponse: Redirect response or HTTP response. + """ + if form.is_valid(): + view_data = self.request.GET.get("view") + emp_ids = self.form.cleaned_data["employee_id"] + cand_id = self.form.cleaned_data["candidate_id"] + interview_date = self.form.cleaned_data["interview_date"] + interview_time = self.form.cleaned_data["interview_time"] + users = [employee.employee_user_id for employee in emp_ids] + notify.send( + self.request.user.employee_get, + recipient=users, + verb=f"You are scheduled as an interviewer for an interview with {cand_id.name} on {interview_date} at {interview_time}.", + verb_ar=f"أنت مجدول كمقابلة مع {cand_id.name} يوم {interview_date} في توقيت {interview_time}.", + verb_de=f"Sie sind als Interviewer für ein Interview mit {cand_id.name} am {interview_date} um {interview_time} eingeplant.", + verb_es=f"Estás programado como entrevistador para una entrevista con {cand_id.name} el {interview_date} a las {interview_time}.", + verb_fr=f"Vous êtes programmé en tant qu'intervieweur pour un entretien avec {cand_id.name} le {interview_date} à {interview_time}.", + icon="people-circle", + redirect=reverse("interview-view"), + ) + if form.instance.pk: + messages.success(self.request, _("Interview Updated Successfully")) + if view_data == "false": + form.save() + return self.HttpResponse( + "" + ) + else: + messages.success(self.request, _("Interview Scheduled successfully.")) + if not view_data: + form.save() + return self.HttpResponse( + "" + ) + form.save() + return self.HttpResponse() + return super().form_valid(form) diff --git a/recruitment/cbv/pipeline.py b/recruitment/cbv/pipeline.py new file mode 100644 index 000000000..39e45b904 --- /dev/null +++ b/recruitment/cbv/pipeline.py @@ -0,0 +1,464 @@ +""" +recruitment/cbv/pipeline.py +""" + +from typing import Any +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.utils.http import urlencode +from django.core.cache import cache as CACHE +from django.contrib import messages +from horilla_views.cbv_methods import login_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, + HorillaTabView, + get_short_uuid, +) +from recruitment import models, filters, forms +from recruitment.templatetags.recruitmentfilters import ( + recruitment_manages, + stage_manages, +) +from recruitment.cbv_decorators import manager_can_enter + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class PipelineView(TemplateView): + """ + PipelineView + """ + + template_name = "cbv/pipeline/pipeline.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentTabView(HorillaTabView): + """ + RecruitmentTabView + """ + + filter_class = filters.RecruitmentFilter + + def __init__(self, **kwargs): + super().__init__(**kwargs) + recruitments = self.filter_class(self.request.GET).qs.filter(is_active=True) + CACHE.set( + self.request.session.session_key + "pipeline", + { + "stages": GetStages.filter_class(self.request.GET).qs.order_by( + "sequence" + ), + "recruitments": recruitments, + "candidates": False, + }, + ) + self.tabs = [] + view_perm = self.request.user.has_perm("recruitment.view_recruitment") + change_perm = self.request.user.has_perm("recruitment.change_recruitment") + add_cand_perm = self.request.user.has_perm("recruitment.add_candidate") + delete_perm = self.request.user.has_perm("recruitment.delete_recruitment") + add_stage_perm = self.request.user.has_perm("recruitment.add_stage") + for rec in recruitments: + rec_manager_perm = recruitment_manages(self.request.user, rec) + stage_manage_perm = stage_manages(self.request.user, rec) + tab = {} + tab["title"] = rec + tab["url"] = reverse("get-stages-recruitment", kwargs={"rec_id": rec.pk}) + tab["badge_label"] = _("Stages") + tab["actions"] = [] + if rec_manager_perm or change_perm: + if add_stage_perm or rec_manager_perm or change_perm: + tab["actions"].append( + { + "action": _("Add Stage"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse('rec-stage-create')}?recruitment_id={rec.pk}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + }, + ) + + tab["actions"].append( + { + "action": _("Manage Stage Order"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse("rec-update-stage-seq", kwargs={"pk": rec.pk})}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + } + ) + + if change_perm or rec_manager_perm or change_perm: + tab["actions"].append( + { + "action": _("Edit"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-get="{reverse("recruitment-update-pipeline", kwargs={"pk": rec.pk})}" + hx-target="#genericModalBody" + style="cursor: pointer;" + """, + }, + ) + + if add_cand_perm or rec_manager_perm or change_perm: + tab["actions"].append( + { + "action": _("Resume Shortlisting"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#bulkResumeUpload" + hx-get="{reverse('view-bulk-resume')}?rec_id={rec.pk}" + hx-target="#bulkResumeUploadBody" + style="cursor: pointer;" + """, + }, + ) + + if change_perm or rec_manager_perm: + if rec.closed: + tab["actions"].append( + { + "action": _("Reopen"), + "attrs": f""" + href="{reverse("recruitment-reopen-pipeline", kwargs={"rec_id": rec.pk})}" + style="cursor: pointer;" + onclick="return confirm('Are you sure you want to reopen this recruitment?');" + """, + }, + ) + else: + tab["actions"].append( + { + "action": _("Close"), + "attrs": f""" + href="{reverse("recruitment-close-pipeline", kwargs={"rec_id": rec.pk})}" + style="cursor: pointer;" + onclick="return confirm('Are you sure you want to close this recruitment?');" + """, + }, + ) + if delete_perm: + tab["actions"].append( + { + "action": _("Delete"), + "attrs": f""" + data-toggle="oh-modal-toggle" + data-target="#deleteConfirmation" + hx-get="{reverse('generic-delete')}?model=recruitment.Recruitment&pk={rec.pk}" + hx-target="#deleteConfirmationBody" + style="cursor: pointer;" + """, + } + ) + if stage_manage_perm or view_perm: + self.tabs.append(tab) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class GetStages(TemplateView): + """ + GetStages + """ + + filter_class = filters.StageFilter + + template_name = "cbv/pipeline/stages.html" + stages = None + + def get(self, request, *args, **kwargs): + """ + get method + """ + rec_id = kwargs["rec_id"] + cache = CACHE.get(request.session.session_key + "pipeline") + if not cache.get("candidates"): + cache["candidates"] = CandidateList.filter_class( + self.request.GET + ).qs.filter(is_active=True) + CACHE.set(request.session.session_key + "pipeline", cache) + + self.stages = cache["stages"].filter(recruitment_id=rec_id) + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["stages"] = self.stages + context["view_id"] = get_short_uuid(6, "hsv") + return context + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class CandidateList(HorillaListView): + """ + CandidateList + """ + + model = models.Candidate + filter_class = filters.CandidateFilter + filter_selected = False + quick_export = False + next_prev = False + custom_empty_template = "cbv/pipeline/empty.html" + header_attrs = { + "action": """ + style="width:413px;" +""", + "mobile": """ + style="width:100px;" +""", + "Stage": """ + style="width:100px;" +""", + "get_interview_count": """ + style="width:200px;" +""", + } + columns = [ + ("Name", "candidate_name", "get_avatar"), + ("Email", "mail_indication"), + ("Stage", "stage_drop_down"), + ("Rating", "rating_bar"), + ("Hired Date", "hired_date"), + ("Scheduled Interview", "get_interview_count"), + ("Job Position", "job_position_id"), + ("Contact", "mobile"), + ] + + default_columns = [ + ("Name", "candidate_name", "get_avatar"), + ("Email", "mail_indication"), + ("Stage", "stage_drop_down"), + ] + + bulk_update_fields = [ + "stage_id", + "hired_date", + ] + + row_attrs = """ + hx-get='{get_details_candidate}' + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + """ + + header_attrs = { + "option": """ + style="width:280px !important" + """ + } + actions = [ + { + "action": _("Schedule Interview"), + "icon": "time-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get = "{get_schedule_interview}" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + """, + }, + { + "action": _("Send Mail"), + "icon": "mail-open-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + hx-get = "{get_send_mail}" + data-toggle="oh-modal-toggle" + data-target="#objectDetailsModal" + hx-target="#objectDetailsModalTarget" + """, + }, + { + "action": _("Add to Skill Zone"), + "icon": "heart-circle-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100 disabled" + data-toggle="oh-modal-toggle" + hx-get="{get_skill_zone_url}" + data-target="#genericModal" + hx-target="#genericModalBody" + """, + }, + { + "action": _("Reject Candidate"), + "icon": "thumbs-down-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + data-toggle="oh-modal-toggle" + hx-get="{get_rejected_candidate_url}" + {rejected_candidate_class} + data-target="#genericModal" + hx-target="#genericModalBody" + """, + }, + { + "action": _("View Note"), + "icon": "newspaper-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100 oh-activity-sidebar__open" + hx-get="{get_view_note_url}" + data-target="#activitySidebar" + hx-target="#activitySidebar" + onclick="$('#activitySidebar').addClass('oh-activity-sidebar--show')" + """, + }, + { + "action": _("Document Request"), + "icon": "document-attach-outline", + "attrs": """ + hx-get="{get_document_request}" + data-target="#genericModal" + hx-target="#genericModalBody" + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + data-toggle="oh-modal-toggle" + """, + }, + { + "action": _("Resume"), + "icon": "document-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100" + href="{get_resume_url}" target="_blank" + """, + }, + ] + records_count_in_tab = False + + def get_bulk_form(self): + form = super().get_bulk_form() + form.fields["stage_id"].queryset = form.fields["stage_id"].queryset.filter( + recruitment_id=self.kwargs["rec_id"] + ) + return form + + def bulk_update_accessibility(self): + """ + Bulk Update accessiblity + """ + if not self.kwargs.get("stage_id"): + return super().bulk_update_accessibility() + first_cand_in_stage = self.queryset.first() + return super().bulk_update_accessibility() or ( + first_cand_in_stage + and ( + self.request.user.employee_get + in first_cand_in_stage.stage_id.stage_managers.all() + or self.request.user.employee_get + in first_cand_in_stage.recruitment_id.recruitment_managers.all() + ) + ) + + records_per_page = 10 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.search_url = self.request.path + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if not self.bulk_update_accessibility(): + context["actions"] = [] + return context + + def get(self, request, *args, **kwargs): + self.selected_instances_key_id = f"selectedCandidateRecords{kwargs['stage_id']}" + return super().get(request, *args, **kwargs) + + def get_queryset(self, *args, **kwargs): + if self.queryset is None: + queryset = CACHE.get(self.request.session.session_key + "pipeline")[ + "candidates" + ].filter(stage_id=self.kwargs["stage_id"]) + queryset = queryset.filter(stage_id=self.kwargs["stage_id"]) + super().get_queryset(queryset=queryset, filtered=True) + + return self.queryset + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class PipelineNav(HorillaNavView): + """ + HorillaNavView + """ + + search_url = reverse_lazy("cbv-pipeline-tab") + nav_title = _("Pipeline") + search_swap_target = "#pipelineContainer" + filter_body_template = "cbv/pipeline/pipeline_filter.html" + filter_instance = filters.RecruitmentFilter() + filter_form_context_name = "form" + apply_first_filter = False + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + if self.request.user.has_perm("recruitment.add_recruitment"): + self.create_attrs = f""" + hx-get="{reverse_lazy('recruitment-create')}?{urlencode({'pipeline': 'true'})}" + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + else: + self.create_attrs = None + + def get_context_data(self, **kwargs): + """ + context data + """ + context = super().get_context_data(**kwargs) + stage_filter_obj = GetStages.filter_class() + candidate_filter_obj = CandidateList.filter_class() + context["stage_filter_obj"] = stage_filter_obj + context["candidate_filter_obj"] = candidate_filter_obj + return context + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + manager_can_enter(perm="recruitment.view_recruitment"), name="dispatch" +) +class ChangeStage(HorillaFormView): + """ + Change Candidate stage + """ + + model = models.Candidate + form_class = forms.StageChangeForm + + def form_valid(self, form): + if form.is_valid(): + messages.success(self.request, _("Stage Updated")) + form.save() + return self.HttpResponse() + messages.info(self.request, _("Stage not updated")) + + return self.HttpResponse() diff --git a/recruitment/cbv/recruitment_survey.py b/recruitment/cbv/recruitment_survey.py new file mode 100644 index 000000000..d6dc1cc1a --- /dev/null +++ b/recruitment/cbv/recruitment_survey.py @@ -0,0 +1,183 @@ +""" +This page handles the cbv methods for recruitment survey page +""" +from typing import Any +from django import forms +from django.contrib import messages +from django.http import HttpResponse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.shortcuts import render +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, +) +from horilla_views.cbv_methods import login_required, permission_required +from recruitment.forms import QuestionForm,TemplateForm +from recruitment.models import RecruitmentSurvey,SurveyTemplate + + + +@method_decorator(login_required,name="dispatch") +@method_decorator(permission_required("recruitment.add_recruitmentsurvey"),name="dispatch") +class QuestionFormView(HorillaFormView): + """ + form view for create button + """ + + form_class = QuestionForm + model = RecruitmentSurvey + new_display_title = _("Survey Questions") + template_name = "cbv/recruitment_survey/survey_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Survey Questions") + return context + + def form_valid(self, form: QuestionForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Survey question updated.") + else: + message = _("New survey question created.") + instance = form.save(commit=False) + instance.save() + instance.recruitment_ids.set(form.recruitment) + instance.template_id.set(form.cleaned_data["template_id"]) + messages.success(self.request, _(message)) + return HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required,name="dispatch") +@method_decorator(permission_required("recruitment.add_recruitmentsurvey"),name="dispatch") +class QuestionDuplicateFormView(HorillaFormView): + """ + form view for create duplicate for asset + """ + + form_class = QuestionForm + model = RecruitmentSurvey + new_display_title = _("Duplicate Survey Questions") + template_name = "cbv/recruitment_survey/survey_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + original_object = RecruitmentSurvey.objects.get(id=self.kwargs["obj_id"]) + form = self.form_class(instance=original_object) + 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 + context["form"] = form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_valid(self, form: QuestionForm) -> HttpResponse: + if form.is_valid(): + message = _("New survey question created.") + instance = form.save(commit=False) + instance.save() + instance.recruitment_ids.set(form.recruitment) + instance.template_id.set(form.cleaned_data["template_id"]) + messages.success(self.request, _(message)) + return HttpResponse("") + return super().form_valid(form) + + + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("recruitment.add_surveytemplate"),name="dispatch") +class SurveyTemplateFormView(HorillaFormView): + """ + form view for create and edit survey templates + """ + + form_class = TemplateForm + model = SurveyTemplate + + def get_form(self, form_class=None): + title = self.request.GET.get("title") + instance = SurveyTemplate.objects.filter(title=title).first() + + if not self.request.POST: + self.form = self.form_class(instance=instance) + else: + self.form = self.form_class(self.request.POST,instance=instance) + return self.form + + + def form_invalid(self, form: Any) -> HttpResponse: + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: TemplateForm) -> HttpResponse: + if form.is_valid(): + message = _("Template saved") + form.save() + messages.success(self.request, _(message)) + return HttpResponse("") + return super().form_valid(form) + + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required("recruitment.add_surveytemplate"),name="dispatch") +class RecruitmentSurveyDetailView(HorillaDetailedView): + """ + detail view of the page + """ + + model = RecruitmentSurvey + title = _("Details") + body = [ + (_("Question"),"question"), + (_("Question Type"),"get_question_type"), + (_("Sequence"),"sequence"), + (_("Recruitment"),"recruitment_col"), + (_("Options"),"options_col",True), + ] + + header = {"title": "question", "subtitle": "","avatar":""} + + + cols = { + "question" : 12 + } + + actions = [ + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--info w-50" + hx-get="{get_edit_url}" + hx-target ="#genericModalBody" + data-target = "#genericModal" + data-toggle ="oh-modal-toggle" + """, + }, + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger w-50" + href ="{get_delete_url}" + onclick="return confirm(' Are you sure want to delete?')" + """, + }, + ] + + + diff --git a/recruitment/cbv/recruitment_view.py b/recruitment/cbv/recruitment_view.py new file mode 100644 index 000000000..79d5c2956 --- /dev/null +++ b/recruitment/cbv/recruitment_view.py @@ -0,0 +1,382 @@ +""" +recruitment +""" + +from typing import Any +from django.http import HttpResponse +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from django import forms +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaListView, + HorillaNavView, + TemplateView, + HorillaFormView, +) +from recruitment.filters import RecruitmentFilter +from recruitment.forms import AddCandidateForm, RecruitmentCreationForm, SkillsForm +from recruitment.models import Candidate, Recruitment, Skill +from recruitment.views.linkedin import delete_post, post_recruitment_in_linkedin + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentView(TemplateView): + """ + Recuitment page + """ + + template_name = "cbv/recruitment/recruitment.html" + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentList(HorillaListView): + """ + List view of recruitment + """ + + model = Recruitment + filter_class = RecruitmentFilter + view_id = "rec-view-container" + + bulk_update_fields = ["vacancy", "start_date", "end_date", "closed"] + + template_name = "cbv/recruitment/rec_main.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-recruitment") + + columns = [ + ("Recruitment", "recruitment_column"), + ("Managers", "managers_column"), + ("Open Jobs", "open_job_col"), + ("Vaccancy", "vacancy"), + ("Total Hires", "tot_hires"), + ("Start Date", "start_date"), + ("End date", "end_date"), + ("Status", "status_col"), + ] + action_method = "rec_actions" + + header_attrs = { + "recruitment_column" : """ + style="width : 200px !important" + """ + } + + row_status_indications = [ + ( + "closed--dot", + _("Closed"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=closed]').val('true'); + $('#applyFilter').click(); + + " + """, + ), + ( + "open--dot", + _("Open"), + """ + onclick=" + $('#applyFilter').closest('form').find('[name=closed]').val('false'); + $('#applyFilter').click(); + + " + """, + ), + ] + + row_status_class = "closed-{closed}" + + sortby_mapping = [ + ("Recruitment", "recruitment_column"), + ("Vaccancy", "vacancy"), + ("Start Date", "start_date"), + ("End date", "end_date"), + ] + + row_attrs = """ + class="oh-permission-table--collapsed" + hx-get='{recruitment_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentNav(HorillaNavView): + """ + For nav bar + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-recruitment") + + self.create_attrs = f""" + hx-get='{reverse_lazy('recruitment-create')}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + nav_title = _("Recruitment") + filter_instance = RecruitmentFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + filter_body_template = "cbv/recruitment/filters.html" + + +class RecruitmentCreationFormExtended(RecruitmentCreationForm): + """ + extended form view for create + """ + + cols = {"title": 12, "description": 12, + "is_published":4, + "optional_profile_image":4, + "optional_resume":4,} + + class Meta: + """ + Meta class to add the additional info + """ + + model = Recruitment + fields = [ + "title", + "description", + "open_positions", + "recruitment_managers", + "start_date", + "end_date", + "vacancy", + "company_id", + "survey_templates", + "skills", + "is_published", + "optional_profile_image", + "optional_resume", + 'publish_in_linkedin', + 'linkedin_account_id', + ] + exclude = ["is_active"] + widgets = { + "start_date": forms.DateInput(attrs={"type": "date"}), + "end_date": forms.DateInput(attrs={"type": "date"}), + "description": forms.Textarea(attrs={"data-summernote": ""}), + } + labels = { + "description": _("Description"), + "start_date": _("Start Date"), + "end_date": _("End Date"), + "survey_templates": _("Survey Templates"), + "is_published": _("Is Published?"), + "vacancy": _("Vacancy"), + "open_positions": _("Job Position"), + "recruitment_managers": _("Managers"), + "optional_profile_image":_("Optional Profile Image?"), + "optional_resume": _("Optional Resume?") + } + + +class RecruitmentNewSkillForm(HorillaFormView): + """ + form view for add new skill + """ + + model = Skill + form_class = SkillsForm + new_display_title = _("Skills") + is_dynamic_create_view = True + + def form_valid(self, form: SkillsForm) -> HttpResponse: + if form.is_valid(): + message = _("New Skill Created Successfully") + form.save() + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentForm(HorillaFormView): + """ + Form View + """ + + model = Recruitment + form_class = RecruitmentCreationFormExtended + new_display_title = _("Add Recruitment") + dynamic_create_fields = [("skills", RecruitmentNewSkillForm)] + template_name = "cbv/recruitment/recruitment_form.html" + + + + def get_context_data(self, **kwargs): + """ + Return context data with optional verbose name for form based on instance state. + """ + context = super().get_context_data(**kwargs) + + if self.form.instance.pk: + self.form_class.verbose_name = "Edit Recruitment" + return context + + def form_valid(self, form: RecruitmentCreationFormExtended) -> HttpResponse: + """ + Process form submission to save or update a Recruitment object and display success message. + """ + if form.is_valid(): + if form.instance.pk: + recruitment = form.save() + recruitment_managers = self.request.POST.getlist("recruitment_managers") + if recruitment_managers: + recruitment.recruitment_managers.set(recruitment_managers) + if (recruitment.publish_in_linkedin and recruitment.linkedin_account_id): + delete_post(recruitment) + post_recruitment_in_linkedin(self.request,recruitment,recruitment.linkedin_account_id) + message = _("Recruitment Updated Successfully") + else: + recruitment = form.save() + recruitment_managers = self.request.POST.getlist("recruitment_managers") + if recruitment_managers: + recruitment.recruitment_managers.set(recruitment_managers) + if (recruitment.publish_in_linkedin and recruitment.linkedin_account_id): + post_recruitment_in_linkedin(self.request,recruitment,recruitment.linkedin_account_id) + message = _("Recruitment Created Successfully") + messages.success(self.request, message) + if self.request.GET.get("pipeline") == "true" : + return HttpResponse("") + return self.HttpResponse() + return super().form_valid(form) + + + +class AddCandidateFormView(HorillaFormView): + """ + form view for add candidate + """ + + form_class = AddCandidateForm + model = Candidate + new_display_title = _("Add Candidate") + + def get_initial(self) -> dict: + initial = super().get_initial() + stage_id = self.request.GET.get("stage_id") + rec_id = self.request.GET.get("rec_id") + initial["stage_id"] = stage_id + initial["rec_id"] = rec_id + return initial + + def form_valid(self, form: AddCandidateForm) -> HttpResponse: + if form.is_valid(): + message = _("Candidate Added successfully.") + form.save() + messages.success(self.request, message) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentFormDuplicate(HorillaFormView): + """ + Duplicate form view + """ + + model = Recruitment + form_class = RecruitmentCreationFormExtended + + def get_context_data(self, **kwargs): + """ + Return context data for duplicating a Recruitment object form with modified initial values. + """ + context = super().get_context_data(**kwargs) + original_object = Recruitment.objects.get(id=self.kwargs["pk"]) + form = self.form_class(instance=original_object) + 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 + context["form"] = form + self.form_class.verbose_name = _("Duplicate") + return context + + def form_valid(self, form: RecruitmentCreationFormExtended) -> HttpResponse: + """ + Process form submission to add a new recruitment. + """ + form = self.form_class(self.request.POST) + if form.is_valid(): + recruitment = form.save() + message = _("Recruitment added") + recruitment.save() + recruitment_managers = self.request.POST.getlist("recruitment_managers") + job_positions = self.request.POST.getlist("open_positions") + if recruitment_managers: + recruitment.recruitment_managers.set(recruitment_managers) + if job_positions: + recruitment.open_positions.set(job_positions) + messages.success(self.request, message) + return self.HttpResponse() + + return super().form_valid(form) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class RecruitmentDetailView(HorillaDetailedView): + """ + detail view of page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + ("Managers", "managers_detail", True), + ("Open Jobs", "open_job_detail", True), + ("Vaccancy", "vacancy"), + ("Total Hires", "tot_hires"), + ("Start Date", "start_date"), + ("End date", "end_date"), + ] + + action_method = "detail_actions" + + model = Recruitment + title = _("Details") + header = { + "title": "title", + "subtitle": "status_col", + "avatar": "get_avatar", + } diff --git a/recruitment/cbv/skill_zone.py b/recruitment/cbv/skill_zone.py new file mode 100644 index 000000000..7287fed59 --- /dev/null +++ b/recruitment/cbv/skill_zone.py @@ -0,0 +1,78 @@ +""" +this page is handling the cbv methods of skill zone page +""" + +from django.contrib import messages +from django.http import HttpResponse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from horilla_views.cbv_methods import login_required +from recruitment.cbv_decorators import manager_can_enter +from horilla_views.generic.cbv.views import ( + HorillaFormView, +) +from recruitment.forms import SkillZoneCandidateForm, SkillZoneCreateForm +from recruitment.models import SkillZone, SkillZoneCandidate + + + +@method_decorator(login_required,name="dispatch") +@method_decorator(manager_can_enter("recruitment.add_skillzone"),name="dispatch") +class SkillZoneFormView(HorillaFormView): + """ + form view for create skill zone + """ + + form_class = SkillZoneCreateForm + model = SkillZone + new_display_title = _("Create Skill Zone") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Skill Zone") + + return context + + def form_valid(self, form: SkillZoneCreateForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Skill Zone updated successfully.") + else: + message = _("Skill Zone created successfully") + form.save() + + messages.success(self.request, _(message)) + return self.HttpResponse("") + return super().form_valid(form) + + +@method_decorator(login_required,name="dispatch") +@method_decorator(manager_can_enter("recruitment.add_skillzonecandidate"),name="dispatch") +class SkillZoneCandidateFormView(HorillaFormView): + """ + form view for create skill zone candidate + """ + + form_class = SkillZoneCandidateForm + model = SkillZoneCandidate + new_display_title = _("Add Candidate to skill zone") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + id = self.kwargs.get("sz_id") + self.form.fields["skill_zone_id"].initial = id + # if self.form.instance.pk: + # self.form_class.verbose_name = _("Update Skill Zone") + return context + + def form_valid(self, form: SkillZoneCandidateForm) -> HttpResponse: + if form.is_valid(): + if form.instance.pk: + message = _("Candidate updated successfully.") + else: + message = _("Candidate added successfully.") + form.save(commit=True) + messages.success(self.request, _(message)) + return self.HttpResponse("") + return super().form_valid(form) diff --git a/recruitment/cbv/skills.py b/recruitment/cbv/skills.py new file mode 100644 index 000000000..c0418129f --- /dev/null +++ b/recruitment/cbv/skills.py @@ -0,0 +1,150 @@ +""" +this page is handling the cbv methods for skills in settings +""" + +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.contrib import messages +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from recruitment.filters import SkillsFilter +from recruitment.forms import SkillsForm +from recruitment.models import Skill +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class SkillsListView(HorillaListView): + """ + list view of the skills in settings + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("skills-list-view") + + model = Skill + filter_class = SkillsFilter + + columns = [(_("SI.No"), "get_sino"), (_("Skill"), "title")] + + row_attrs = """ + id="skillsTr{get_delete_instance}" + """ + + actions = [ + { + "action": _("Edit"), + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + hx-get='{get_update_url}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + """, + }, + { + "action": _("Delete"), + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100 text-danger" + hx-post="{get_delete_url}" + hx-swap="delete" + hx-confirm="Are you sure want to delete this skill?" + hx-target="#skillsTr{get_delete_instance}" + """, + }, + ] + + header_attrs = { + "title": """ style="width:200px !important" """, + "action": """ style="width:200px !important" """ + } + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.view_recruitment"), name="dispatch" +) +class SkillsNavView(HorillaNavView): + """ + navbar of skills view + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("skills-list-view") + self.create_attrs = f""" + onclick = "event.stopPropagation();" + data-toggle="oh-modal-toggle" + data-target="#genericModal" + hx-target="#genericModalBody" + hx-get="{reverse('settings-create-skills')}" + """ + + nav_title = _("Skills") + search_swap_target = "#listContainer" + filter_instance = SkillsFilter() + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required(perm="recruitment.add_recruitment"), name="dispatch" +) +class SkillsCreateForm(HorillaFormView): + """ + form view for creating and update skills in settings + """ + + model = Skill + form_class = SkillsForm + new_display_title = _("Skills") + + def get_context_data(self, **kwargs): + """ + Add form to context, initializing with instance if it exists. + """ + context = super().get_context_data(**kwargs) + form = self.form_class() + if self.form.instance.pk: + form = self.form_class(instance=self.form.instance) + self.form_class.verbose_name = _("Update Skills") + context[form] = form + return context + + def form_invalid(self, form: Any) -> HttpResponse: + """ + Handles and renders form errors or defers to superclass. + """ + if self.form.instance.pk: + self.form_class.verbose_name = _("Update Skills") + if not form.is_valid(): + errors = form.errors.as_data() + return render( + self.request, self.template_name, {"form": form, "errors": errors} + ) + return super().form_invalid(form) + + def form_valid(self, form: SkillsForm) -> HttpResponse: + """ + Handle valid form submission. + """ + if form.is_valid(): + if form.instance.pk: + messages.success(self.request, _("Skill updated")) + else: + messages.success(self.request, _("Skill created successfully!")) + form.save() + return self.HttpResponse() + return super().form_valid(form) diff --git a/recruitment/cbv/stage_view.py b/recruitment/cbv/stage_view.py new file mode 100644 index 000000000..dc57a9d16 --- /dev/null +++ b/recruitment/cbv/stage_view.py @@ -0,0 +1,314 @@ +""" +Stage.py +""" + +from typing import Any +import contextlib +from django import forms +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.contrib import messages +from notifications.signals import notify +from employee.models import Employee +from horilla_views.cbv_methods import login_required, permission_required +from horilla_views.generic.cbv.views import ( + HorillaDetailedView, + HorillaFormView, + HorillaListView, + HorillaNavView, + TemplateView, +) +from recruitment.filters import StageFilter +from recruitment.forms import StageCreationForm +from recruitment.models import Stage + + +@method_decorator(login_required, name="dispatch") +@method_decorator(permission_required(perm="recruitment.view_stage"), name="dispatch") +class StageView(TemplateView): + """ + Stage + """ + + template_name = "cbv/stages/stages.html" + + +class StageList(HorillaListView): + """ + List view of stage + """ + + bulk_update_fields = [ + "stage_managers", + ] + + model = Stage + filter_class = StageFilter + + def get_queryset(self): + """ + Returns a filtered queryset of active recruitments. + """ + queryset = super().get_queryset() + queryset = queryset.filter(recruitment_id__is_active=True) + return queryset + + columns = [ + ("Title", "title_col"), + ("Managers", "managers_col"), + ("Type", "get_type"), + ] + sortby_mapping = [ + ("Type", "get_type"), + ] + action_method = "actions_col" + + row_status_indications = [ + ( + "hired--dot", + "Hired", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=stage_type]').val('hired'); + $('#applyFilter').click(); + " + """, + ), + ( + "cancelled--dot", + "Cancelled", + """ + onclick=" + $('#applyFilter').closest('form').find('[name=stage_type]').val('cancelled'); + $('#applyFilter').click(); + " + """, + ), + ( + "interview--dot", + "Interview", + """ + onclick=" $('#applyFilter').closest('form').find('[name=stage_type]').val('interview'); + $('#applyFilter').click(); + " + """, + ), + ( + "test--dot", + "Test", + """ + onclick=" $('#applyFilter').closest('form').find('[name=stage_type]').val('test'); + $('#applyFilter').click(); + " + """, + ), + ( + "initial--dot", + "Initial", + """ + onclick=" $('#applyFilter').closest('form').find('[name=stage_type]').val('initial'); + $('#applyFilter').click(); + " + """, + ), + ] + + row_status_class = "stage-type-{stage_type}" + + row_attrs = """ + class="oh-permission-table--collapsed" + hx-get='{stage_detail_view}?instance_ids={ordered_ids}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + header_attrs = { + "title_col": """ + style='width:250px !important' + """, + "managers_col": """ + style='width:250px !important' + """, + "get_type": """ + style='width:250px !important' + """, + "action": """ + style="width:250px !important" + """, + } + + +class StageNav(HorillaNavView): + """ + For nav bar + """ + + template_name = "cbv/stages/stage_main.html" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.search_url = reverse("list-stage") + self.create_attrs = f""" + hx-get='{reverse_lazy('rec-stage-create')}' + hx-target="#genericModalBody" + data-target="#genericModal" + data-toggle="oh-modal-toggle" + """ + + nav_title = _("Stage") + filter_instance = StageFilter() + filter_form_context_name = "form" + search_swap_target = "#listContainer" + filter_body_template = "cbv/stages/filter.html" + + group_by_fields = [("recruitment_id", "Recruitment")] + + +class StageFormView(HorillaFormView): + """ + Form View + """ + + model = Stage + form_class = StageCreationForm + new_display_title = _("Add Stage") + + def get_context_data(self, **kwargs): + """ + Returns context with a form for creating or editing a stage. + """ + context = super().get_context_data(**kwargs) + rec_id = self.request.GET.get("recruitment_id") + self.form.fields["recruitment_id"].initial = rec_id + if self.form.instance.pk: + self.form_class.verbose_name = _("Edit Stage") + self.form_class(instance=self.form.instance) + context["form"] = self.form + return context + + def form_valid(self, form: StageCreationForm) -> HttpResponse: + """ + Handles valid form submission, updating or saving a stage. + """ + if form.is_valid(): + if form.instance.pk: + stage = form.save() + stage.save() + stage_managers = self.request.POST.getlist("stage_managers") + if stage_managers: + stage.stage_managers.set(stage_managers) + message = _("Stage updated") + else: + stage_obj = form.save() + stage_obj.stage_managers.set( + Employee.objects.filter(id__in=form.data.getlist("stage_managers")) + ) + stage_obj.save() + recruitment_obj = stage_obj.recruitment_id + rec_stages = ( + Stage.objects.filter(recruitment_id=recruitment_obj, is_active=True) + .order_by("sequence") + .last() + ) + if rec_stages.sequence is None: + stage_obj.sequence = 1 + else: + stage_obj.sequence = rec_stages.sequence + 1 + stage_obj.save() + message = _("Stage added") + with contextlib.suppress(Exception): + managers = stage_obj.stage_managers.select_related( + "employee_user_id" + ) + users = [employee.employee_user_id for employee in managers] + notify.send( + self.request.user.employee_get, + recipient=users, + verb=f"Stage {stage_obj} is updated on recruitment {stage_obj.recruitment_id},\ + You are chosen as one of the managers", + verb_ar=f"تم تحديث المرحلة {stage_obj} في التوظيف\ + {stage_obj.recruitment_id}، تم اختيارك كأحد المديرين", + verb_de=f"Stufe {stage_obj} wurde in der Rekrutierung {stage_obj.recruitment_id}\ + aktualisiert. Sie wurden als einer der Manager ausgewählt", + verb_es=f"La etapa {stage_obj} ha sido actualizada en la contratación\ + {stage_obj.recruitment_id}. Has sido elegido/a como uno de los gerentes", + verb_fr=f"L'étape {stage_obj} a été mise à jour dans le recrutement\ + {stage_obj.recruitment_id}. Vous avez été choisi(e) comme l'un des responsables", + icon="people-circle", + redirect=reverse("pipeline"), + ) + messages.success(self.request, message) + return self.HttpResponse() + return super().form_valid(form) + + +class StageDuplicateForm(HorillaFormView): + """ + Duplicate form view + """ + + model = Stage + form_class = StageCreationForm + + def get_context_data(self, **kwargs): + """ + Prepares form context for duplicating a stage. + """ + context = super().get_context_data(**kwargs) + original_object = Stage.objects.get(id=self.kwargs["pk"]) + form = self.form_class(instance=original_object) + 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 + context["form"] = form + self.form_class.verbose_name = "Duplicate" + return context + + def form_valid(self, form: StageCreationForm) -> HttpResponse: + """ + Handles valid submission of a stage creation form. + """ + form = self.form_class(self.request.POST) + if form.is_valid(): + message = "Stage added" + stage = form.save() + stage.save() + stage_managers = self.request.POST.getlist("stage_managers") + if stage_managers: + stage.stage_managers.set(stage_managers) + messages.success(self.request, _(message)) + return self.HttpResponse() + + return super().form_valid(form) + + +class StageDetailView(HorillaDetailedView): + """ + detail view of page + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.body = [ + ("Title", "stage"), + ("Managers", "detail_managers_col"), + ("Type", "get_type"), + ] + + action_method = "detail_action" + + model = Stage + title = _("Details") + header = { + "title": "recruitment_id", + "subtitle": "Stages", + "avatar": "get_avatar", + } diff --git a/recruitment/cbv_decorators.py b/recruitment/cbv_decorators.py new file mode 100644 index 000000000..74c5add19 --- /dev/null +++ b/recruitment/cbv_decorators.py @@ -0,0 +1,111 @@ +from django.http import HttpResponse +from django.shortcuts import render +from django.contrib import messages +from employee.models import Employee +from horilla_views.cbv_methods import decorator_with_arguments +from recruitment.models import Recruitment, Stage +from horilla.horilla_middlewares import _thread_locals + + +@decorator_with_arguments +def manager_can_enter(function, perm): + """ + Decorator that checks if the user has the specified permission or is a manager. + + Args: + perm (str): The permission to check. + + Returns: + function: The decorated function. + + Raises: + None + + """ + + def _function(self, *args, **kwargs): + """ + Inner function that performs the permission and manager check. + + Args: + request (HttpRequest): The request object. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + HttpResponse: The response from the decorated function. + + """ + request = getattr(_thread_locals, "request") + if not getattr(self, "request", None): + self.request = request + user = request.user + employee = Employee.objects.filter(employee_user_id=user).first() + is_manager = ( + Stage.objects.filter(stage_managers=employee).exists() + or Recruitment.objects.filter(recruitment_managers=employee).exists() + ) + if user.has_perm(perm) or is_manager: + return function(self, *args, **kwargs) + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function + + +@decorator_with_arguments +def all_manager_can_enter(function, perm): + """ + Decorator that checks if the user has the specified permission or is a manager. + + Args: + perm (str): The permission to check. + + Returns: + function: The decorated function. + + Raises: + None + + """ + + def _function(self, *args, **kwargs): + """ + Inner function that performs the permission and manager check. + + Args: + request (HttpRequest): The request object. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + HttpResponse: The response from the decorated function. + + """ + request = getattr(_thread_locals, "request") + if not getattr(self, "request", None): + self.request = request + user = request.user + employee = Employee.objects.filter(employee_user_id=user).first() + is_manager = ( + Stage.objects.filter(stage_managers=employee).exists() + or Recruitment.objects.filter(recruitment_managers=employee).exists() + or request.user.employee_get.onboardingstage_set.exists() + or request.user.employee_get.onboarding_task.exists() + ) + if user.has_perm(perm) or is_manager: + return function(self, *args, **kwargs) + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function diff --git a/recruitment/decorators.py b/recruitment/decorators.py index e75f8171c..7cea0c536 100644 --- a/recruitment/decorators.py +++ b/recruitment/decorators.py @@ -105,6 +105,56 @@ def manager_can_enter(function, perm): return _function +@decorator_with_arguments +def all_manager_can_enter(function, perm): + """ + Decorator that checks if the user has the specified permission or is a manager. + + Args: + perm (str): The permission to check. + + Returns: + function: The decorated function. + + Raises: + None + + """ + + def _function(request, *args, **kwargs): + """ + Inner function that performs the permission and manager check. + + Args: + request (HttpRequest): The request object. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + HttpResponse: The response from the decorated function. + + """ + user = request.user + employee = Employee.objects.filter(employee_user_id=user).first() + is_manager = ( + Stage.objects.filter(stage_managers=employee).exists() + or Recruitment.objects.filter(recruitment_managers=employee).exists() + or request.user.employee_get.onboardingstage_set.exists() + or request.user.employee_get.onboarding_task.exists() + ) + if user.has_perm(perm) or is_manager: + return function(request, *args, **kwargs) + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function + + @decorator_with_arguments def recruitment_manager_can_enter(function, perm): """ diff --git a/recruitment/filters.py b/recruitment/filters.py index 000ab4406..d7d85fdd9 100644 --- a/recruitment/filters.py +++ b/recruitment/filters.py @@ -12,12 +12,15 @@ from django import forms from django.utils.translation import gettext_lazy as _ from base.filters import FilterSet +from horilla.filters import HorillaFilterSet, filter_by_name from recruitment.models import ( Candidate, InterviewSchedule, LinkedInAccount, Recruitment, RecruitmentSurvey, + RejectReason, + Skill, SkillZone, SkillZoneCandidate, Stage, @@ -27,7 +30,7 @@ from recruitment.models import ( # from django.forms.widgets import Boo -class CandidateFilter(FilterSet): +class CandidateFilter(HorillaFilterSet): """ Filter set class for Candidate model @@ -36,6 +39,8 @@ class CandidateFilter(FilterSet): """ name = django_filters.CharFilter(field_name="name", lookup_expr="icontains") + search = django_filters.CharFilter(method="search_by_name", lookup_expr="icontains") + start_onboard = django_filters.CharFilter( method="start_onboard_method", lookup_expr="icontains" ) @@ -118,12 +123,16 @@ class CandidateFilter(FilterSet): ).distinct() return queryset - def start_onboard_method(self, queryset, _, value): + def search_by_name(self, queryset, _, value): """ - This method will include the candidates whether they are on the onboarding pipline stage + search by name method """ - - return queryset.filter(onboarding_stage__isnull=False) + queryset = ( + queryset.filter(name__icontains=value) + | queryset.filter(stage_id__stage__icontains=value) + | queryset.filter(stage_id__recruitment_id__title__icontains=value) + ) + return queryset.distinct() class Meta: """ @@ -236,7 +245,7 @@ BOOLEAN_CHOICES = ( ) -class RecruitmentFilter(FilterSet): +class RecruitmentFilter(HorillaFilterSet): """ Filter set class for Recruitment model @@ -311,9 +320,12 @@ class RecruitmentFilter(FilterSet): first_name = parts[0] last_name = " ".join(parts[1:]) if len(parts) > 1 else "" - job_queryset = queryset.filter( - open_positions__job_position__icontains=value - ) | queryset.filter(title__icontains=value) + job_queryset = ( + queryset.filter(open_positions__job_position__icontains=value) + | queryset.filter(title__icontains=value) + | queryset.filter(stage_set__stage__icontains=value) + | queryset.filter(stage_set__candidate__name__icontains=value) + ) if first_name and last_name: queryset = queryset.filter( recruitment_managers__employee_first_name__icontains=first_name, @@ -356,7 +368,29 @@ class RecruitmentFilter(FilterSet): return queryset.distinct() -class StageFilter(FilterSet): +class SkillsFilter(FilterSet): + + search = django_filters.CharFilter(field_name="title", lookup_expr="icontains") + + class Meta: + model = Skill + fields = [ + "title", + ] + + +class RejectReasonFilter(FilterSet): + + search = django_filters.CharFilter(field_name="title", lookup_expr="icontains") + + class Meta: + model = RejectReason + fields = [ + "title", + ] + + +class StageFilter(HorillaFilterSet): """ Filter set class for Stage model @@ -391,7 +425,13 @@ class StageFilter(FilterSet): parts = value.split() first_name = parts[0] last_name = " ".join(parts[1:]) if len(parts) > 1 else "" - recruitment_query = queryset.filter(recruitment_id__title__icontains=value) + recruitment_query = ( + queryset.filter(recruitment_id__title__icontains=value) + | queryset.filter(candidate__name__icontains=value) + | queryset.filter( + recruitment_id__stage_set__candidate__name__icontains=value + ) + ) # Filter the queryset by first name and last name stage_queryset = queryset.filter(stage__icontains=value) if first_name and last_name: @@ -409,7 +449,7 @@ class StageFilter(FilterSet): ) queryset = queryset | stage_queryset | recruitment_query - return queryset + return queryset.distinct() def pipeline_search(self, queryset, _, value): """ @@ -423,7 +463,7 @@ class StageFilter(FilterSet): return queryset.distinct() -class SurveyFilter(FilterSet): +class SurveyFilter(HorillaFilterSet): """ SurveyFIlter """ @@ -628,7 +668,7 @@ class SkillZoneCandFilter(FilterSet): ).distinct() -class InterviewFilter(FilterSet): +class InterviewFilter(HorillaFilterSet): """ Filter set class for Candidate model diff --git a/recruitment/forms.py b/recruitment/forms.py index df13f7ee1..d9695c9dd 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -36,6 +36,7 @@ from django.utils.translation import gettext_lazy as _ from base.forms import Form from base.forms import ModelForm as BaseModelForm from base.methods import reload_queryset +from base.widgets import CustomTextInputWidget from employee.filters import EmployeeFilter from employee.models import Employee from horilla import horilla_middlewares @@ -231,16 +232,12 @@ class RecruitmentCreationForm(BaseModelForm): Form for Recruitment model """ - # survey_templates = forms.ModelMultipleChoiceField( - # queryset=SurveyTemplate.objects.all(), - # widget=forms.SelectMultiple(), - # label=_("Survey Templates"), - # required=False, - # ) - # linkedin_account_id = forms.ModelChoiceField( - # queryset=LinkedInAccount.objects.filter(is_active=True) - # label=_('') - # ) + cols = { + "is_published": 4, + "optional_profile_image": 4, + "optional_resume": 4, + } + class Meta: """ Meta class to add the additional info @@ -265,7 +262,6 @@ class RecruitmentCreationForm(BaseModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - reload_queryset(self.fields) if not self.instance.pk: self.fields["recruitment_managers"] = HorillaMultiSelectField( @@ -295,11 +291,6 @@ class RecruitmentCreationForm(BaseModelForm): # def create_option(self, *args,**kwargs): # option = super().create_option(*args,**kwargs) - # if option.get('value') == "create": - # option['attrs']['class'] = 'text-danger' - - # return option - def clean(self): if isinstance(self.fields["recruitment_managers"], HorillaMultiSelectField): ids = self.data.getlist("recruitment_managers") @@ -748,6 +739,8 @@ class QuestionForm(ModelForm): QuestionForm """ + cols = {"options": 12, "template_id": 12, "question": 12} + verbose_name = "Survey Questions" recruitment = forms.ModelMultipleChoiceField( @@ -836,6 +829,20 @@ class QuestionForm(ModelForm): initial=initial, ) + def create_options_field_more(option_key, initial=None): + self.fields[option_key] = forms.CharField( + widget=CustomTextInputWidget( + delete_url="add-remove-options-field", + attrs={ + "name": option_key, + "id": f"{option_key}", + "class": "oh-input w-100", + }, + ), + required=False, + initial=initial, + ) + if instance: split_options = instance.options.split(",") for i, option in enumerate(split_options): @@ -843,7 +850,7 @@ class QuestionForm(ModelForm): create_options_field("options", option) else: self.option_count += 1 - create_options_field(f"options{i}", option) + create_options_field_more(f"options{i}", option) if instance: self.fields["recruitment"].initial = instance.recruitment_ids.all() @@ -901,6 +908,10 @@ class TemplateForm(BaseModelForm): TemplateForm """ + cols = {"title": 12, "description": 12, "company_id": 12} + + verbose_name = "Template" + class Meta: model = SurveyTemplate fields = "__all__" @@ -988,6 +999,8 @@ class CandidateExportForm(forms.Form): class SkillZoneCreateForm(BaseModelForm): + cols = {"title": 12, "description": 12, "company_id": 12} + class Meta: """ Class Meta for additional options @@ -998,8 +1011,10 @@ class SkillZoneCreateForm(BaseModelForm): exclude = ["is_active"] -class SkillZoneCandidateForm(BaseModelForm): - verbose_name = _("Skill Zone Candidate") +class SkillZoneCandidateForm(ModelForm): + + cols = {"skill_zone_id": 12, "candidate_id": 12, "reason": 12} + verbose_name = "Skill Zone Candidate" candidate_id = forms.ModelMultipleChoiceField( queryset=Candidate.objects.all(), widget=forms.SelectMultiple, @@ -1045,6 +1060,11 @@ class SkillZoneCandidateForm(BaseModelForm): + " / " + self.instance.skill_zone_id.title ) + self.fields["candidate_id"] = forms.ModelChoiceField( + queryset=Candidate.objects.all(), + widget=forms.Select(attrs={"class": "oh-select oh-select2 w-100"}), + label=_("Candidate"), + ) def save(self, commit: bool = True) -> SkillZoneCandidate: @@ -1066,12 +1086,15 @@ class SkillZoneCandidateForm(BaseModelForm): return self.instance -class ToSkillZoneForm(BaseModelForm): - verbose_name = _("Add To Skill Zone") +class ToSkillZoneForm(ModelForm): + + verbose_name = "Add To Skill Zone" skill_zone_ids = forms.ModelMultipleChoiceField( queryset=SkillZone.objects.all(), label=_("Skill Zones") ) + cols = {"reason": 12, "skill_zone_ids": 12} + class Meta: """ Class Meta for additional options @@ -1125,6 +1148,8 @@ class RejectReasonForm(ModelForm): RejectReasonForm """ + cols = {"title": 12, "description": 12, "company_id": 12} + verbose_name = "Reject Reason" class Meta: @@ -1148,6 +1173,8 @@ class RejectedCandidateForm(ModelForm): verbose_name = "Rejected Candidate" + cols = {"reject_reason_id": 12, "description": 12} + class Meta: model = RejectedCandidate fields = "__all__" @@ -1172,6 +1199,16 @@ class ScheduleInterviewForm(BaseModelForm): ScheduleInterviewForm """ + cols = { + "interview_date": 12, + "interview_time": 12, + "candidate_id": 12, + "description": 12, + "employee_id": 12, + } + + verbose_name = "Schedule Interview" + class Meta: model = InterviewSchedule fields = "__all__" @@ -1185,6 +1222,19 @@ class ScheduleInterviewForm(BaseModelForm): self.fields["interview_time"].widget = forms.TimeInput( attrs={"type": "time", "class": "oh-input w-100"} ) + candidate_attr = { + "hx-include": "#InterviewCreateForm", + "hx-target": "#id_employee_id_parent_div", + "hx-get": "/recruitment/get-interview-managers", + "hx-swap": "innerHTML", + "hx-select": "#id_employee_id_parent_div", + "hx-trigger": "change, load delay:300ms", + } + + if self.instance.pk: + candidate_attr["hx-get"] += f"?pk={self.instance.pk}" + + self.fields["candidate_id"].widget.attrs.update(candidate_attr) def clean(self): @@ -1239,6 +1289,10 @@ class ScheduleInterviewForm(BaseModelForm): class SkillsForm(ModelForm): + cols = { + "title": 12, + } + class Meta: model = Skill fields = ["title"] @@ -1311,6 +1365,22 @@ class CandidateDocumentForm(ModelForm): return table_html +class StageChangeForm(forms.ModelForm): + """ + StageChangeForm + """ + + class Meta: + """ + Meta class for additional options + """ + + model = Candidate + fields = [ + "stage_id", + ] + + class LinkedInAccountForm(BaseModelForm): """ LinkedInAccount form diff --git a/recruitment/methods.py b/recruitment/methods.py index 4c2497ebd..fd6952a9c 100644 --- a/recruitment/methods.py +++ b/recruitment/methods.py @@ -70,3 +70,15 @@ def update_rec_template_grp(upt_template_ids, template_groups, rec_id): ) for survey in rec_surveys_templates: survey.recruitment_ids.add(recruitment_obj) + + +def in_all_managers(request): + """ + Check the user in any recruitment/onboarding related managers + """ + return ( + request.user.employee_get.stage_set.exists() + or request.user.employee_get.recruitment_set.exists() + or request.user.employee_get.onboardingstage_set.exists() + or request.user.employee_get.onboarding_task.exists() + ) diff --git a/recruitment/models.py b/recruitment/models.py index d63485d64..918dac52d 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -8,26 +8,30 @@ This module is used to register models for recruitment app import json import os import re -from datetime import date +from datetime import date, datetime, timezone +from urllib.parse import urlencode from uuid import uuid4 import django import requests from django import forms from django.conf import settings +from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.files.storage import default_storage from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models.signals import m2m_changed, post_save from django.dispatch import receiver -from django.http import JsonResponse -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy +from django.utils import timezone as tz +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company, JobPosition from employee.models import Employee +from horilla.horilla_middlewares import _thread_locals from horilla.models import HorillaModel from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog @@ -111,6 +115,40 @@ class Skill(HorillaModel): self.title = title.capitalize() super().save(*args, **kwargs) + def get_sino(self): + """ + for get serial nos + """ + all_instances = list(Skill.objects.order_by("id")) + sino = all_instances.index(self) + 1 + return sino + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("settings-update-skills", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + base_url = reverse_lazy("delete-skills") + skill_id = self.pk + url = f"{base_url}?ids={skill_id}" + return url + + def get_delete_instance(self): + """ + to get instance for delete + """ + + return self.pk + + def __str__(self) -> str: + return f"{self.title}" + class Meta: verbose_name = _("Skill") verbose_name_plural = _("Skills") @@ -239,7 +277,7 @@ class Recruitment(HorillaModel): if not self.is_event_based and self.job_position_id is not None: self.open_positions.add(self.job_position_id) - return title + return str(title) def clean(self): if self.title is None: @@ -274,6 +312,119 @@ class Recruitment(HorillaModel): """ return self.stage_set.order_by("sequence") + def recruitment_column(self): + """ + This method for get custom column for recruitment. + """ + + return render_template( + path="cbv/recruitment/recruitment_col.html", + context={"instance": self}, + ) + + def recruitment_detail_view(self): + """ + detail view + """ + url = reverse("recruitment-detail-view", kwargs={"pk": self.pk}) + return url + + def managers_column(self): + """ + This method for get custom column for managers. + """ + + return render_template( + path="cbv/recruitment/managers_col.html", + context={"instance": self}, + ) + + def managers_detail(self): + """ + manager in detail view + """ + employees = self.recruitment_managers.all() + if employees: + employee_names_string = "
    ".join( + [str(employee) for employee in employees] + ) + managers_title = _("Managers") + return f'{managers_title}{employee_names_string}' + else: + return "" + + def managers(self): + manager_list = self.recruitment_managers.all() + formatted_managers = [ + f"
    {i + 1}. {manager}
    " for i, manager in enumerate(manager_list) + ] + return "".join(formatted_managers) + + def detail_actions(self): + """ + This method for get custom column for managers. + """ + + return render_template( + path="cbv/recruitment/detail_action.html", + context={"instance": self}, + ) + + def open_job_col(self): + """ + This method for get custom column for open jobs. + """ + + return render_template( + path="cbv/recruitment/open_jobs.html", + context={"instance": self}, + ) + + def open_job_detail(self): + """ + open jobs in detail view + """ + jobs = self.open_positions.all() + if jobs: + jobs_names_string = "
    ".join([str(job) for job in jobs]) + job_title = _("Open Jobs") + return f'{job_title}{jobs_names_string}' + else: + return "" + + def tot_hires(self): + """ + This method for get custom column for Total hires. + """ + + return render_template( + path="cbv/recruitment/total_hires.html", + context={"instance": self}, + ) + + def status_col(self): + if self.closed: + return "Closed" + else: + return "Open" + + def rec_actions(self): + """ + This method for get custom column for actions. + """ + + return render_template( + path="cbv/recruitment/actions.html", + context={"instance": self}, + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.title}&background=random" + return url + def is_vacancy_filled(self): """ This method is used to check wether the vaccancy for the recruitment is completed or not @@ -344,6 +495,83 @@ class Stage(HorillaModel): ) } + def stage_detail_view(self): + """ + detail view + """ + url = reverse("stage-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_action(self): + """ + For answerable employees column + """ + + return render_template( + path="cbv/stages/detail_action.html", + context={"instance": self}, + ) + + def title_col(self): + """ + This method for get custome coloumn for title. + """ + return render_template( + path="cbv/stages/title.html", + context={"instance": self}, + ) + + def managers_col(self): + """ + This method for get custome coloumn for managers. + """ + + return render_template( + path="cbv/stages/managers.html", + context={"instance": self}, + ) + + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = ( + f"https://ui-avatars.com/api/?name={self.recruitment_id}&background=random" + ) + return url + + def detail_managers_col(self): + """ + Manager in detail view + """ + employees = self.stage_managers.all() + employee_names_string = "
    ".join([str(employee) for employee in employees]) + return employee_names_string + + def actions_col(self): + """ + This method for get custome coloumn for actions. + """ + + return render_template( + path="cbv/stages/actions.html", + context={"instance": self}, + ) + + def get_type(self): + """ + Display type + """ + stage_types = [ + ("initial", _("Initial")), + ("test", _("Test")), + ("interview", _("Interview")), + ("cancelled", _("Cancelled")), + ("hired", _("Hired")), + ] + + return dict(stage_types).get(self.stage_type) + class Candidate(HorillaModel): """ @@ -486,6 +714,295 @@ class Candidate(HorillaModel): def __str__(self): return f"{self.name}" + def stage_drop_down(self): + """ + Stage drop down + """ + request = getattr(_thread_locals, "request", None) + all_rec_stages = getattr(request, "all_rec_stages", {}) + if all_rec_stages.get(self.stage_id.recruitment_id.pk) is None: + stages = Stage.objects.filter(recruitment_id=self.stage_id.recruitment_id) + all_rec_stages[self.stage_id.recruitment_id.pk] = stages + request.all_rec_stages = all_rec_stages + return render_template( + path="cbv/pipeline/stage_drop_down.html", + context={ + "instance": self, + "stages": request.all_rec_stages[self.stage_id.recruitment_id.pk], + }, + ) + + def rating_bar(self): + """ + Rating bar + """ + return render_template( + path="cbv/pipeline/rating.html", context={"instance": self} + ) + + def get_interview_count(self): + """ + Scheduled interviews count + """ + return render_template( + path="cbv/pipeline/count_of_interviews.html", context={"instance": self} + ) + + def mail_indication(self): + """ + Rating bar + """ + return render_template( + path="cbv/pipeline/mail_status.html", context={"instance": self} + ) + + def candidate_name(self): + """ + Rating bar + """ + now = tz.now() + return render_template( + path="cbv/pipeline/candidate_column.html", + context={"instance": self, "now": now}, + ) + + def get_contact(self): + """ + to get contact no of candidates + """ + return self.mobile + + def get_resume_url(self): + return self.resume.url + + def onboarding_portal_html(self): + return format_html( + '
    {}/4
    ', + self.onboarding_portal.count, + ) + + def rating(self): + """ + This method for get custome coloumn for rating. + """ + + return render_template( + path="cbv/candidates/rating.html", + context={"instance": self}, + ) + + def onboarding_status_col(self): + """ + This method for get custome coloumn for status. + """ + + return render_template( + path="cbv/onboarding_view/status.html", + context={"instance": self}, + ) + + def onboarding_task_col(self): + """ + This method for get custome coloumn for tasks. + """ + from onboarding.models import CandidateStage, CandidateTask + + cand_stage = self.onboarding_stage.id + cand_stage_obj = CandidateStage.objects.get(id=cand_stage) + choices = CandidateTask.choice + + return render_template( + path="cbv/onboarding_view/task.html", + context={ + "instance": self, + "candidate": cand_stage_obj, + "choices": choices, + "single_view": True, + }, + ) + + def archive_status(self): + """ + archive status + """ + if self.is_active: + return "Archive" + else: + return "Un-Archive" + + def resume_pdf(self): + """ + This method for get custome coloumn for resume. + """ + + return render_template( + path="cbv/candidates/resume.html", + context={"instance": self}, + ) + + def options(self): + """ + This method for get custom coloumn for options. + """ + + request = getattr(_thread_locals, "request", None) + mails = getattr(request, "mails", None) + + if not mails: + mails = list(Candidate.objects.values_list("email", flat=True)) + setattr(request, "mails", mails) + + emp_list = User.objects.filter(username__in=mails).values_list( + "email", flat=True + ) + + return render_template( + path="cbv/candidates/option.html", + context={"instance": self, "emp_list": emp_list}, + ) + + def get_profile_url(self): + """ + This method to get profile url + """ + url = reverse_lazy("candidate-view", kwargs={"pk": self.pk}) + return url + + def get_update_url(self): + """ + This method to get update url + """ + url = reverse_lazy("rec-candidate-update", kwargs={"cand_id": self.pk}) + return url + + def get_skill_zone_url(self): + """ + This method to get update url + """ + url = reverse_lazy("to-skill-zone", kwargs={"cand_id": self.pk}) + return url + + def get_rejected_candidate_url(self): + """ + This method to get the update URL with cand_id as a query parameter. + """ + base_url = reverse_lazy("add-to-rejected-candidates") + query_params = urlencode({"candidate_id": self.pk}) + return f"{base_url}?{query_params}" + + def get_document_request(self): + """ + This method to get the update URL with cand_id as a query parameter. + """ + base_url = reverse_lazy("candidate-document-request") + query_params = urlencode({"candidate_id": self.pk}) + return f"{base_url}?{query_params}" + + def get_view_note_url(self): + """ + This method to get update url + """ + url = reverse_lazy("view-note", kwargs={"cand_id": self.pk}) + return url + + def get_individual_url(self): + """ + This method to get update url + """ + url = reverse_lazy("candidate-view-individual", kwargs={"cand_id": self.pk}) + return url + + def get_push_url(self): + """ + This method to get update url + """ + url = reverse_lazy("candidate-view-individual", kwargs={"cand_id": self.pk}) + return url + + def get_convert_to_emp(self): + """ + This method to get covert to employee url + """ + url = reverse_lazy("candidate-conversion", kwargs={"cand_id": self.pk}) + return url + + def get_add_to_skill(self): + """ + This method to get add to skill zone employee url + """ + url = reverse_lazy("to-skill-zone", kwargs={"cand_id": self.pk}) + return url + + def get_add_to_reject(self): + """ + This method to get add to reject zone employee url + """ + url = reverse_lazy("add-to-rejected-candidates") + return f"{url}?candidate_id={self.pk}" + + def get_archive_url(self): + """ + This method to get archive url + """ + + if self.is_active: + action = "archive" + else: + action = "un-archive" + + message = f"Do you want to {action} this candidate?" + url = reverse_lazy("rec-candidate-archive", kwargs={"cand_id": self.pk}) + + return f"'{url}','{message}'" + + def get_delete_url(self): + """ + This method to get delete url + """ + url = reverse_lazy("generic-delete") + return url + + def get_self_tracking_url(self): + """ + This method to get self tracking url + """ + url = reverse_lazy( + "candidate-self-status-tracking", kwargs={"cand_id": self.pk} + ) + return url + + def get_document_request_doc(self): + """ + This method to get document request url + """ + url = reverse_lazy("candidate-document-request") + f"?candidate_id={self.pk}" + return url + + def is_employee_converted(self): + """ + The method to get converted employee + """ + request = getattr(_thread_locals, "request", None) + if not getattr(request, "employees", None): + request.employees = Employee.objects.all() + + if request.employees.filter(email=self.email).exists(): + return 'style="background-color: #f1ffd5;"' + + def get_details_candidate(self): + """ + Candidate detail + """ + url = reverse_lazy("candidate-detail", kwargs={"pk": self.pk}) + return url + + def get_send_mail(self): + """ + Candidate detail + """ + url = reverse_lazy("send-mail", kwargs={"cand_id": self.pk}) + return url + def is_offer_rejected(self): """ Is offer rejected checking method @@ -563,6 +1080,10 @@ class Candidate(HorillaModel): .first() ) + def get_schedule_interview(self): + url = reverse_lazy("interview-schedule", kwargs={"cand_id": self.pk}) + return url + def get_interview(self): """ This method is used to get the interview dates and times for the candidate for the mail templates @@ -588,6 +1109,13 @@ class Candidate(HorillaModel): else: return "" + def candidate_interview_view(self): + interviews = InterviewSchedule.objects.filter(candidate_id=self.pk) + return render_template( + path="cbv/pipeline/interview_template.html", + context={"instance": self, "interviews": interviews}, + ) + def save(self, *args, **kwargs): if self.stage_id is not None: self.hired = self.stage_id.stage_type == "hired" @@ -628,6 +1156,70 @@ class Candidate(HorillaModel): super().save(*args, **kwargs) + def last_email(self): + """ + for last send mail column + + """ + + return render_template( + path="cbv/onboarding_candidates/cand_email.html", + context={"instance": self}, + ) + + def date_of_joining(self): + """ + for joining date column + + """ + + return render_template( + path="cbv/onboarding_candidates/date_of_joining.html", + context={"instance": self}, + ) + + def probation_date(self): + """ + for probation date column + + """ + + return render_template( + path="cbv/onboarding_candidates/probation_date.html", + context={"instance": self}, + ) + + def offer_letter(self): + """ + for offer letter column + + """ + + return render_template( + path="cbv/onboarding_candidates/offer_letter.html", + context={"instance": self}, + ) + + def rejected_candidate_class(self): + """ + Returns the appropriate style and title attributes for rejected candidates. + """ + if self.is_offer_rejected(): + return f'style="background: #ff4500a3 !important; color: white;" title="{_("Added In Rejected Candidates")}"' + else: + return f'title="{_("Add To Rejected Candidates")}"' + + def actions(self): + """ + for actions column + + """ + + return render_template( + path="cbv/onboarding_candidates/actions.html", + context={"instance": self}, + ) + class Meta: """ Meta class to add the additional info @@ -667,6 +1259,26 @@ class RejectReason(HorillaModel): def __str__(self) -> str: return self.title + def get_update_url(self): + """ + This method to get update url + """ + + url = reverse_lazy("update-reject-reason-view", kwargs={"pk": self.pk}) + return url + + def get_delete_url(self): + """ + This method to get delete url + """ + base_url = reverse_lazy("delete-reject-reasons") + rej_id = self.pk + url = f"{base_url}?id={rej_id}" + return url + + def get_instance_id(self): + return self.id + class Meta: verbose_name = _("Reject Reason") verbose_name_plural = _("Reject Reasons") @@ -778,6 +1390,40 @@ class RecruitmentSurvey(HorillaModel): def __str__(self) -> str: return str(self.question) + def options_col(self): + if self.type == "options" or self.type == "multiple": + return ( + f"
    Options
    {self.options}
    " + if self.options + else "" + ) + return "" + + def get_edit_url(self): + + url = reverse( + "recruitment-survey-question-template-edit", kwargs={"pk": self.pk} + ) + return url + + def get_delete_url(self): + + url = reverse( + "recruitment-survey-question-template-delete", kwargs={"survey_id": self.pk} + ) + return url + + def recruitment_col(self): + """ + Manager in detail view + """ + recruitment = self.recruitment_ids.all() + recruitment_string = "
    ".join([str(rec) for rec in recruitment]) + return recruitment_string + + def get_question_type(self): + return dict(self.question_types).get(self.type) + def choices(self): """ Used to split the choices @@ -876,6 +1522,28 @@ class SkillZone(HorillaModel): def __str__(self) -> str: return self.title + def get_avatar(self): + """ + Method will retun the api to the avatar or path to the profile image + """ + url = f"https://ui-avatars.com/api/?name={self.title}&background=random" + return url + + def candidate_count_display(self): + count = self.skillzonecandidate_set.count() + if count != 1: + return f"{count} { _('Candidates') }" + else: + return f"{count} { _('Candidate') }" + + def get_skill_zone_url(self): + """ + This method returns the skill zone URL with the title as a query parameter. + """ + base_url = reverse("skill-zone-view") + query_string = urlencode({"search": self.title}) + return f"{base_url}?{query_string}" + class SkillZoneCandidate(HorillaModel): """ @@ -986,6 +1654,106 @@ class InterviewSchedule(HorillaModel): def __str__(self) -> str: return f"{self.candidate_id} -Interview." + def candidate_custom_col(self): + """ + method for candidate coloumn + """ + return render_template( + path="cbv/interview/candidate_custom_col.html", + context={"instance": self}, + ) + + def interviewer_custom_col(self): + """ + method for interviewer coloumn + """ + return render_template( + path="cbv/interview/interviewer_custom_col.html", + context={"instance": self}, + ) + + def custom_color(self): + """ + Custom background color for all rows with hover effect + """ + # interviews = InterviewSchedule.objects.filter( + # employee_id=self.user.employee_get.id + # ) + request = getattr(_thread_locals, "request", None) + if not getattr(self, "request", None): + self.request = request + user = request.user + if user.employee_get in self.employee_id.all(): + color = "rgba(255, 166, 0, 0.158)" + hovering = "white" + + return ( + f'style="background-color: {color};" ' + f"onmouseover=\"this.style.backgroundColor='{hovering}';\" " + f"onmouseout=\"this.style.backgroundColor='{color}';\"" + ) + + def interviewer_detail(self): + """ + interviewer in detail view + """ + employees = self.employee_id.all() + employee_names_string = ", ".join([str(employee) for employee in employees]) + return employee_names_string + + def detail_subtitle(self): + """ + Return subtitle for detail view + """ + return ( + f"{self.candidate_id.recruitment_id} / {self.candidate_id.job_position_id}" + ) + + def get_description(self): + """ + get description + """ + if self.description: + return self.description + else: + return _("None") + + def status_custom_col(self): + """ + method for status coloumn + """ + now = datetime.now(tz=timezone.utc if settings.USE_TZ else None) + return render_template( + path="cbv/interview/status_custom_col.html", + context={"instance": self, "now": now}, + ) + + def custom_action_col(self): + """ + method for actions coloumn + """ + return render_template( + path="cbv/interview/interview_actions.html", + context={"instance": self}, + ) + + def detail_view(self): + """ + for detail view + """ + + url = reverse("interview-detail-view", kwargs={"pk": self.pk}) + return url + + def detail_view_actions(self): + """ + detail view actions + """ + return render_template( + path="cbv/interview/detail_view_actions.html", + context={"instance": self}, + ) + class Meta: verbose_name = _("Schedule Interview") verbose_name_plural = _("Schedule Interviews") diff --git a/recruitment/sidebar.py b/recruitment/sidebar.py index 16a210b99..6a76854f2 100644 --- a/recruitment/sidebar.py +++ b/recruitment/sidebar.py @@ -25,7 +25,7 @@ SUBMENUS = [ }, { "menu": _("Recruitment Pipeline"), - "redirect": reverse("pipeline"), + "redirect": reverse("cbv-pipeline"), "accessibility": "recruitment.sidebar.pipeline_accessibility", }, { diff --git a/recruitment/templates/candidate/export_filter.html b/recruitment/templates/candidate/export_filter.html index 3a90059a5..a5f79a9e8 100644 --- a/recruitment/templates/candidate/export_filter.html +++ b/recruitment/templates/candidate/export_filter.html @@ -248,5 +248,11 @@ {% trans "Export" %}
  • +<<<<<<< HEAD +
    + + +=======
    +>>>>>>> 99553272eb5ec68396db80a610cdcd0901a4dfca diff --git a/recruitment/templates/candidate/individual.html b/recruitment/templates/candidate/individual.html index e3360989c..57cc5fdd4 100644 --- a/recruitment/templates/candidate/individual.html +++ b/recruitment/templates/candidate/individual.html @@ -1,4 +1,6 @@ -{% extends 'index.html' %} {% load i18n %} {% load horillafilters %} {% load static %} {% block content %} {% load recruitmentfilters %} +{% extends "index.html" %} +{% block content %} +{% include "generic/components.html" %} + +{% include "generic/components.html" %} + +{% include "generic/components.html" %}
    @@ -191,7 +274,7 @@ >
  • -{% endblock content %} +{% endblock content %} {% endcomment %} diff --git a/recruitment/templates/candidate/interview_form.html b/recruitment/templates/candidate/interview_form.html index 9a43b73f9..da9170bc3 100644 --- a/recruitment/templates/candidate/interview_form.html +++ b/recruitment/templates/candidate/interview_form.html @@ -9,7 +9,7 @@ id="skillform"> {{form.as_p}} - +>>>>>>> a557b8a4c05c90515a575c7585620c822d2503d6 diff --git a/recruitment/templates/candidate/rating_tab.html b/recruitment/templates/candidate/rating_tab.html index 57e934a18..0623aa2f2 100644 --- a/recruitment/templates/candidate/rating_tab.html +++ b/recruitment/templates/candidate/rating_tab.html @@ -39,10 +39,19 @@ {% endfor %} {% else %} -
    +
    + Page not found. 404. +
    {% trans "There are no ratings to display at the moment." %}
    +
    +
    + + +{% comment %}
    {% trans " There are no ratings to display at the moment." %}
    -
    + {% endcomment %} {% endif %} diff --git a/recruitment/templates/cbv/candidates/candidates.html b/recruitment/templates/cbv/candidates/candidates.html new file mode 100644 index 000000000..e1c5bf385 --- /dev/null +++ b/recruitment/templates/cbv/candidates/candidates.html @@ -0,0 +1,314 @@ + + +{% extends "index.html" %} +{% load i18n %}{% load static recruitmentfilters %} + + +{% block content %} + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + + +
    + +
    + + + + + + +{% endblock content %} \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/export.html b/recruitment/templates/cbv/candidates/export.html new file mode 100644 index 000000000..27f419c18 --- /dev/null +++ b/recruitment/templates/cbv/candidates/export.html @@ -0,0 +1,30 @@ + +{% load i18n %}{% load static recruitmentfilters %} +
    +
    +

    + {% trans "Export Candidates" %} +

    + +
    +
    + {% csrf_token %} {% include 'candidate/export_filter.html'%} + +
    +
    +
    +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/filter.html b/recruitment/templates/cbv/candidates/filter.html new file mode 100644 index 000000000..fb1e17f7f --- /dev/null +++ b/recruitment/templates/cbv/candidates/filter.html @@ -0,0 +1,184 @@ +{% load i18n %} +{% load static %} + +
    +
    +
    + {% trans 'Candidates' %} +
    +
    +
    +
    +
    + + {{ form.mobile }} +
    +
    + + {{ form.hired_date }} +
    +
    + + +
    +
    + + {{ form.hired }} +
    +
    +
    + + {{form.rejected_candidate__reject_reason_id}} +
    +
    +
    +
    +
    + + {{ form.email }} +
    + +
    + + {{ form.gender }} +
    + +
    + + +
    + +
    + + {{ form.canceled }} +
    +
    +
    + + {{form.offer_letter_status}} +
    +
    +
    +
    +
    +
    +
    +
    + {% trans 'Recruitment' %} +
    +
    +
    +
    +
    + + {{ form.recruitment_id }} +
    + +
    + + {{ form.job_position_id }} +
    + +
    + + {{ form.start_date }} +
    +
    + + {{ form.recruitment_id__closed }} +
    + +
    + + {{ form.stage_id__stage_type }} +
    +
    + + {{ form.stage_id__stage_managers }} +
    +
    +
    +
    + + {{ form.stage_id }} +
    +
    + + {{ form.job_position_id__department_id }} +
    + +
    + + {{ form.recruitment_id__company_id }} +
    + +
    + + {{ form.recruitment_id__recruitment_managers }} +
    +
    + + {{ form.end_date }} +
    +
    + + {{ form.skillzonecandidate_set__skill_zone_id }} +
    +
    +
    +
    +
    +
    +
    + {% trans 'Survey' %} +
    +
    +
    +
    +
    + + {{ form.survey_answer_by }} +
    +
    + {% comment %}
    +
    + + {{ form.survey_response }} +
    +
    {% endcomment %} +
    +
    +
    + +
    +
    + {% trans 'Advanced' %} +
    +
    +
    +
    +
    + + {{ form.scheduled_from }} +
    +
    + + {{ form.is_active }} +
    +
    +
    +
    + + {{ form.scheduled_till }} +
    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/option.html b/recruitment/templates/cbv/candidates/option.html new file mode 100644 index 000000000..e3c9ad584 --- /dev/null +++ b/recruitment/templates/cbv/candidates/option.html @@ -0,0 +1,77 @@ +{% load basefilters %} +{% load i18n %} +{% load recruitmentfilters %} +{% if perms.recruitment.change_candidate %} +
    + {% if not instance.email in emp_list and not instance.start_onboard %} + + + {% else %} + + {% endif %} + {% if instance.email in emp_list %} + + {% else %} + + {% endif %} + {% if check_candidate_self_tracking %} + {% if perms.recruitment.view_candidate or request.user|is_stagemanager %} + + + + {% endif %} + {% endif %} + {% if instance.email in emp_list %} + + {% else %} + {% if perms.recruitment.add_rejectedcandidate or request.user|is_stagemanager %} + + {% endif %} + {% endif %} + +
    + {% endif %} \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/profile_about_tab.html b/recruitment/templates/cbv/candidates/profile_about_tab.html new file mode 100644 index 000000000..7d8d5a32b --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_about_tab.html @@ -0,0 +1,154 @@ +{% load i18n %} + +
    +
    +
    +
    + {% trans "Personal Information" %} +
    +
    +
      +
    • + + + {% trans "Date of Birth" %} + + {{candidate.dob}} +
    • +
    • + + + {% trans "Gender" %} + + {{candidate.gender|capfirst}} +
    • +
    • + + + {% trans "Address" %} + +

      + {{candidate.address}} +

      +
    • +
    • + + + {% trans "Country" %} + + + {{candidate.country}} + +
    • +
    • + + + {% trans "State" %} + + + {{candidate.state}} + +
    • +
    • + + + {% trans "Portfolio" %} + + + {{candidate.portfolio}} + +
    • +
    +
    +
    +
    +
    +
    +
    + {% trans "Recruitment Information" %} +
    +
    +
    +
    +
      +
    • + + + {% trans "Recruitment" %} + + {{candidate.recruitment_id}} +
    • +
    • + + + + {% trans "Department" %} + + {{candidate.job_position_id.department_id}} +
    • +
    • + + + {% trans "Source" %} + + {% if candidate.get_source_display != None %} + {{candidate.get_source_display}} + {% endif %} +
    • +
    +
    + +
    +
      +
    • + + + {% trans "Current Stage" %} + + {{candidate.stage_id}} +
    • +
    • + + + {% trans "Job Position" %} + + {{candidate.job_position_id}} +
    • +
    • + + + {% trans "Referral" %} + + {{candidate.referral}} +
    • +
    +
    +
    +
    +
    +
    +
    diff --git a/recruitment/templates/cbv/candidates/profile_interview_tab.html b/recruitment/templates/cbv/candidates/profile_interview_tab.html new file mode 100644 index 000000000..f81bbf17d --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_interview_tab.html @@ -0,0 +1,104 @@ +{% load i18n %} +{% load static %} +{% include "generic/components.html" %} +{% comment %} {% include "generic/components.html" %} {% endcomment %} +
    +{% if candidate.candidate_interview.exists %} +
    + {{candidate}}'s {% trans "Scheduled Interviews" %} +
    +
    + +
    + +
      + {% for interview_schedule in candidate.candidate_interview.all %} +
    1. + + + + + + + + {{forloop.counter}}. {% trans "Date" %} : {{ interview_schedule.interview_date }} + {% trans "Time" %} : {{ interview_schedule.interview_time }} + {% trans "Interviewer" %} : + {% for interviewer in interview_schedule.employee_id.all %} +  radio_button_checked {{ interviewer }} + {% endfor %} +
      + {% if interview_schedule.description %} + {% trans "Description" %} : {{ interview_schedule.description }} + {% endif %} + + {% if interview_schedule.completed %} +
      + check_circle + {% trans "Interview Completed" %} +
      + {% elif interview_schedule.interview_date|date:"Y-m-d" < now|date:"Y-m-d" %} +
      + dangerous + {% trans "Expired Interview" %} +
      + {% elif interview_schedule.interview_date|date:"Y-m-d" > now|date:"Y-m-d" %} +
      + schedule + {% trans "Upcoming Interview" %} +
      + {% elif interview_schedule.interview_date|date:"Y-m-d" == now|date:"Y-m-d" and not interview_schedule.completed %} +
      + today + {% trans "Interview Today" %} +
      + {% endif %} + +
    2. + + {% endfor %} +
    +{% else %} +
    + {{candidate}}'s {% trans "Scheduled Interviews" %} +
    +
    + +
    + + +
    +
    + Page not found. 404. +
    {% trans "No interviews are scheduled for this candidate." %}
    +
    +
    + + {% comment %}
    + +
    {% trans "No interviews are scheduled for this candidate" %}
    +
    {% endcomment %} +{% endif %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/profile_notes_tab.html b/recruitment/templates/cbv/candidates/profile_notes_tab.html new file mode 100644 index 000000000..43b0b2dce --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_notes_tab.html @@ -0,0 +1,4 @@ +{% load i18n %} +
    +{% include "candidate/individual_view_note.html" %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/profile_onboarding_tab.html b/recruitment/templates/cbv/candidates/profile_onboarding_tab.html new file mode 100644 index 000000000..2ba0e7ce9 --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_onboarding_tab.html @@ -0,0 +1,82 @@ +{% load i18n %} +{% load static %} +{% load horillafilters %} + +
    +
    +
    +
    +
    +
    +
    +
    + {% if candidate.candidate_task.exists %} + {% for task in candidate.candidate_task.all %} +
    +
    +
    +
    + {{ task.onboarding_task_id.task_title }} +
    + + + +
    +
    +
    + {% endfor %} + {% else %} +
    +
    + Page not found. 404. +
    {% trans "There are no tasks assigned for this candidate." %}
    +
    +
    + {% endif %} +
    +
    +
    + + + + \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/profile_resume_tab.html b/recruitment/templates/cbv/candidates/profile_resume_tab.html new file mode 100644 index 000000000..6dccdd86b --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_resume_tab.html @@ -0,0 +1,17 @@ +{% load i18n %} +{% load static %} +{% if candidate.resume %} + +{% else %} + +
    +
    + Page not found. 404. +
    {% trans "This candidate does not have a resume on file." %}
    +
    +
    + {% endif %} + diff --git a/recruitment/templates/cbv/candidates/profile_survey_tab.html b/recruitment/templates/cbv/candidates/profile_survey_tab.html new file mode 100644 index 000000000..04907118c --- /dev/null +++ b/recruitment/templates/cbv/candidates/profile_survey_tab.html @@ -0,0 +1,67 @@ +{% load i18n %} +{% load horillafilters %} +{% load static %} + +{% if candidate.recruitmentsurveyanswer_set.all %} + {% for surveys in candidate.recruitmentsurveyanswer_set.all %} +
    + {% for question, answer_list in surveys.answer.items %} + {% if question != "csrfmiddlewaretoken" %} +
    +
    + {% if question|slice:"0:11" == "percentage_" %} + {{ question|slice:"11:" }} + {% elif question|slice:"0:7" == "rating_" %} + {{ question|slice:"7:" }} + {% elif question|slice:"0:5" == "file_" %} + {{ question|slice:"5:" }} + {% else %} + {{ question }} + {% endif %} +
    +
    + {% if answer_list|length > 1 %} + {{ answer_list|join:", " }} + {% else %} + {% if answer_list.0 == "on" or answer_list.0 == "off"%} + {{ answer_list.0|on_off }} + {% elif question|slice:"0:11" == "percentage_" %} + {{ answer_list.0 }}% + {% elif question|slice:"0:5" == "file_" %} + {{ answer_list.0 }} + {% elif question|slice:"0:7" == "rating_" %} +
    + + + + + + + + + + +
    + {% else %} + {{ answer_list.0 }} + {% endif %} + {% endif %} +
    +
    + {% endif %} + {% endfor %} +
    + {% endfor %} + {% else %} +
    +
    + Page not found. 404. +
    {% trans "No survey templates have been established yet." %}
    +
    +
    + {% endif %} + {% comment %}
    + +
    {% trans "No survey templates have been established yet." %}
    +
    {% endcomment %} + \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/rating.html b/recruitment/templates/cbv/candidates/rating.html new file mode 100644 index 000000000..ddbb8edb7 --- /dev/null +++ b/recruitment/templates/cbv/candidates/rating.html @@ -0,0 +1,16 @@ +{% load basefilters %} +{% load i18n %} +{% load recruitmentfilters%} +{% with instance.request.user.employee_get.candidate_rating.all as candidate_ratings %} +
    +
    +
    + {% for i in "54321" %} + + + {% endfor %} +
    + +
    +
    +{% endwith %} \ No newline at end of file diff --git a/recruitment/templates/cbv/candidates/resume.html b/recruitment/templates/cbv/candidates/resume.html new file mode 100644 index 000000000..d140c02fe --- /dev/null +++ b/recruitment/templates/cbv/candidates/resume.html @@ -0,0 +1,13 @@ + +
    + +   View + +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/interview/candidate_custom_col.html b/recruitment/templates/cbv/interview/candidate_custom_col.html new file mode 100644 index 000000000..0e482796a --- /dev/null +++ b/recruitment/templates/cbv/interview/candidate_custom_col.html @@ -0,0 +1,12 @@ +{% load i18n %} +
    +
    + + {{instance.candidate_id}} +
    +
    + + diff --git a/recruitment/templates/cbv/interview/detail_view_actions.html b/recruitment/templates/cbv/interview/detail_view_actions.html new file mode 100644 index 000000000..129d8962e --- /dev/null +++ b/recruitment/templates/cbv/interview/detail_view_actions.html @@ -0,0 +1,27 @@ +{% load basefilters %} +{% load i18n %} +{% if perms.recruitment.change_interviewschedule or request.user.employee_get in instance.employee_id.all %} + + + +{% endif %} +{% if perms.recruitment.delete_interviewschedule %} +
    +{% csrf_token %} + + + + +{% endif %} + diff --git a/recruitment/templates/cbv/interview/forms/form_hx.html b/recruitment/templates/cbv/interview/forms/form_hx.html new file mode 100644 index 000000000..35019d51a --- /dev/null +++ b/recruitment/templates/cbv/interview/forms/form_hx.html @@ -0,0 +1,8 @@ +{{form.employee_id}} + \ No newline at end of file diff --git a/recruitment/templates/cbv/interview/inherit_script.html b/recruitment/templates/cbv/interview/inherit_script.html new file mode 100644 index 000000000..aac8782ce --- /dev/null +++ b/recruitment/templates/cbv/interview/inherit_script.html @@ -0,0 +1,12 @@ +
    + {% include 'generic/horilla_list_table.html' %} +
    + \ No newline at end of file diff --git a/recruitment/templates/cbv/interview/interview_actions.html b/recruitment/templates/cbv/interview/interview_actions.html new file mode 100644 index 000000000..dbca9d2be --- /dev/null +++ b/recruitment/templates/cbv/interview/interview_actions.html @@ -0,0 +1,25 @@ +{% load i18n %} +
    + + {% if perms.recruitment.change_interviewschedule or request.user.employee_get in instance.employee_id.all %} + + {% endif %} + + {% if perms.recruitment.delete_interviewschedule %} + + {% csrf_token %} + + + {% endif %} +
    diff --git a/recruitment/templates/cbv/interview/interview_filter.html b/recruitment/templates/cbv/interview/interview_filter.html new file mode 100644 index 000000000..9f8a80915 --- /dev/null +++ b/recruitment/templates/cbv/interview/interview_filter.html @@ -0,0 +1,44 @@ +{%load i18n %} +{% load static %} +
    +
    +
    {% trans "Interview" %}
    +
    +
    +
    + +
    + + {{form.candidate_id}} +
    + +
    + + {{form.scheduled_from}} +
    + +
    +
    + +
    + + {{form.employee_id}} +
    + +
    + + {{form.scheduled_till}} +
    + +
    +
    +
    +
    +
    + {% comment %} {% endcomment %} diff --git a/recruitment/templates/cbv/interview/interview_home_view.html b/recruitment/templates/cbv/interview/interview_home_view.html new file mode 100644 index 000000000..bd094a877 --- /dev/null +++ b/recruitment/templates/cbv/interview/interview_home_view.html @@ -0,0 +1,43 @@ +{% extends "index.html" %} +{% load static %} +{% load i18n %} +{% block content %} + +
    +
    + + +{% include "generic/components.html" %} + + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + + + + +{% endblock content %} \ No newline at end of file diff --git a/recruitment/templates/cbv/interview/interviewer_custom_col.html b/recruitment/templates/cbv/interview/interviewer_custom_col.html new file mode 100644 index 000000000..1f9b5dbd1 --- /dev/null +++ b/recruitment/templates/cbv/interview/interviewer_custom_col.html @@ -0,0 +1,35 @@ +{% load i18n %} + +
    +{% for employee in instance.employee_id.all %} +
    + +
    +
    + Baby C. +
    + + {{employee.get_full_name|truncatechars:15}} +
    + {% if perms.recruitment.change_interviewschedule or request.user.employee_get in instance.employee_id.all %} + + + + {% endif %} +
    +
    + {% endfor %} + {{instance.employee_id.all|length}} {% trans "Interviewers" %} +
    + + \ No newline at end of file diff --git a/recruitment/templates/cbv/interview/status_custom_col.html b/recruitment/templates/cbv/interview/status_custom_col.html new file mode 100644 index 000000000..5d94283fb --- /dev/null +++ b/recruitment/templates/cbv/interview/status_custom_col.html @@ -0,0 +1,24 @@ +{% load i18n %} +
    + {% if instance.completed %} +
    + check_circle + {% trans "Interview Completed" %} +
    + {% elif instance.interview_date|date:"Y-m-d" < now|date:"Y-m-d" %} +
    + dangerous + {% trans "Expired Interview" %} +
    + {% elif instance.interview_date|date:"Y-m-d" > now|date:"Y-m-d" %} +
    + schedule + {% trans "Upcoming Interview" %} +
    + {% elif instance.interview_date|date:"Y-m-d" == now|date:"Y-m-d" and not instance.completed %} +
    + today + {% trans "Interview Today" %} +
    + {% endif %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/candidate_column.html b/recruitment/templates/cbv/pipeline/candidate_column.html new file mode 100644 index 000000000..7ad6fb7e9 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/candidate_column.html @@ -0,0 +1,23 @@ +{% load i18n %} +
    +{% for interview_schedule in instance.candidate_interview.all %} + {% if interview_schedule.interview_date|date:"Y-m-d" == now|date:"Y-m-d" %} +
    + + + alarm_on + + + {% trans "INTERVIEW : Today at" %} {{interview_schedule.interview_time}} {% trans "with" %} + {% for emp in interview_schedule.employee_id.all %} {{emp}}, {% endfor %} + + +
    + {% endif %} + {% endfor %} + + + {{instance|truncatechars:15}} + +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/count_of_interviews.html b/recruitment/templates/cbv/pipeline/count_of_interviews.html new file mode 100644 index 000000000..0da64711f --- /dev/null +++ b/recruitment/templates/cbv/pipeline/count_of_interviews.html @@ -0,0 +1,4 @@ +{% load i18n %} +
    + {% trans "Interviews Scheduled" %} : {{instance.candidate_interview.count}} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/empty.html b/recruitment/templates/cbv/pipeline/empty.html new file mode 100644 index 000000000..9dccecfa4 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/empty.html @@ -0,0 +1,9 @@ +{% load i18n %} +
    +
    + {% comment %}

    {% trans "No Records found" %}

    {% endcomment %} +

    + {% trans "No records found." %} +

    +
    +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/interview_template.html b/recruitment/templates/cbv/pipeline/interview_template.html new file mode 100644 index 000000000..545425f8f --- /dev/null +++ b/recruitment/templates/cbv/pipeline/interview_template.html @@ -0,0 +1,29 @@ + + +{% if interviews %} +
    +
    +
    +
    Sl No.
    +
    Date
    +
    Time
    +
    Is Completed
    +
    +
    + {% for interview in interviews %} +
    +
    +
    {{ forloop.counter }}
    +
    {{ interview.interview_date }}
    +
    {{ interview.interview_time }}
    +
    {% if interview.completed %}Yes{% else %}No{% endif %}
    +
    +
    + {% endfor %} +
    +{% endif %} diff --git a/recruitment/templates/cbv/pipeline/mail_status.html b/recruitment/templates/cbv/pipeline/mail_status.html new file mode 100644 index 000000000..cd600ff3a --- /dev/null +++ b/recruitment/templates/cbv/pipeline/mail_status.html @@ -0,0 +1,15 @@ + + {{instance.email|truncatechars:10}} + +{% if instance.get_last_sent_mail %} + + + +{% endif %} diff --git a/recruitment/templates/cbv/pipeline/pipeline.html b/recruitment/templates/cbv/pipeline/pipeline.html new file mode 100644 index 000000000..e0b0cd9f5 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/pipeline.html @@ -0,0 +1,95 @@ +{% extends "index.html" %} +{% load i18n %} +{% block content %} + + +{% include "generic/components.html" %} + +
    + +
    +
    + +
    +
    + +
    +
    + + +{% endblock content %} \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/pipeline_filter.html b/recruitment/templates/cbv/pipeline/pipeline_filter.html new file mode 100644 index 000000000..cc2a7fb6a --- /dev/null +++ b/recruitment/templates/cbv/pipeline/pipeline_filter.html @@ -0,0 +1,171 @@ +{% load i18n %} {% load static %} +
    +
    +
    {% trans 'Recruitment' %}
    +
    +
    +
    +
    + + {{ form.recruitment_managers }} +
    +
    + + {{ form.start_date }} +
    +
    + + {{ form.start_from }} +
    +
    + + {{ form.closed }} +
    +
    + +
    +
    + + {{ form.company_id }} +
    +
    + + {{ form.end_date }} +
    +
    + + {{ form.end_till }} +
    +
    + + {{ form.is_published }} +
    +
    +
    +
    +
    + +
    +
    {% trans 'Stage' %}
    +
    +
    +
    +
    + + {{ stage_filter_obj.form.recruitment_id }} +
    +
    + + {{ stage_filter_obj.form.stage_type }} +
    +
    + +
    +
    + + {{ stage_filter_obj.form.stage_managers }} +
    +
    +
    +
    +
    + +
    +
    {% trans 'Candidates' %}
    +
    +
    +
    +
    + + +
    +
    +
    + + {{ candidate_filter_obj.form.rejected_candidate__reject_reason_id }} +
    +
    + + {{ candidate_filter_obj.form.hired }} +
    +
    + + {{ candidate_filter_obj.form.gender }} +
    +
    + + {{ candidate_filter_obj.form.job_position_id }} +
    +
    +
    +
    +
    + + +
    +
    + + {{ candidate_filter_obj.form.offer_letter_status }} +
    +
    + + {{ candidate_filter_obj.form.canceled }} +
    +
    + + {{ candidate_filter_obj.form.candidate_rating__rating }} +
    +
    + + {{ candidate_filter_obj.form.job_position_id__department_id }} +
    +
    +
    +
    +
    +
    + + + diff --git a/recruitment/templates/cbv/pipeline/rating.html b/recruitment/templates/cbv/pipeline/rating.html new file mode 100644 index 000000000..fea6f6f4f --- /dev/null +++ b/recruitment/templates/cbv/pipeline/rating.html @@ -0,0 +1,36 @@ +{% load i18n recruitmentfilters horillafilters %} +
    + {% with request.user.employee_get.candidate_rating.all as candidate_ratings %} + {% if candidate_ratings|has_candidate_rating:instance %} +
    + {% csrf_token %} +
    +
    + {% for i in "54321" %} + + + {% endfor %} +
    + + +
    +
    + {% else %} +
    + {% csrf_token %} +
    +
    + {% for i in "54321" %} + + + {% endfor %} +
    + + +
    +
    + {% endif %} + {% endwith %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/pipeline/recruitment_tabs.html b/recruitment/templates/cbv/pipeline/recruitment_tabs.html new file mode 100644 index 000000000..0a1c8248b --- /dev/null +++ b/recruitment/templates/cbv/pipeline/recruitment_tabs.html @@ -0,0 +1,119 @@ +{% load i18n generic_template_filters %} +
    + {% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %} +
    + +
    +
      + {% for tab in tabs %} +
    • + {{tab.title}} +
      +
      + + 0 + +
      + {% if tab.actions %} +
      + + +
      + {% endif %} +
      +
    • + {% endfor %} +
    +
    + {% for tab in tabs %} +
    +
    +
    + {% endfor %} +
    +
    + + + + +
    diff --git a/recruitment/templates/cbv/pipeline/stage_drop_down.html b/recruitment/templates/cbv/pipeline/stage_drop_down.html new file mode 100644 index 000000000..391510b15 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/stage_drop_down.html @@ -0,0 +1,17 @@ +{% load i18n %} +{% load basefilters %} +{% load recruitmentfilters %} +{% if perms.recruitment.change_recruitment or request.user.employee_get in instance.recruitment_id.recruitment_managers.all or request.user.employee_get in instance.stage_id.stage_managers.all %} +
    + +
    +{% else %} + {{instance.stage_id}} +{% endif %} diff --git a/recruitment/templates/cbv/pipeline/stage_order.html b/recruitment/templates/cbv/pipeline/stage_order.html new file mode 100644 index 000000000..25398b4a6 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/stage_order.html @@ -0,0 +1,70 @@ +{% load static i18n %} +
    + + {% trans "Update Stage Order" %} + + +
    +
    +
    +
      + {% for stage in stages %} +
    • + + {{stage.sequence}}. + {{stage}} +
    • + {% endfor %} +
    +
    + +
    +
    +
    + diff --git a/recruitment/templates/cbv/pipeline/stages.html b/recruitment/templates/cbv/pipeline/stages.html new file mode 100644 index 000000000..14c74cd10 --- /dev/null +++ b/recruitment/templates/cbv/pipeline/stages.html @@ -0,0 +1,65 @@ +{% load i18n recruitmentfilters %} +
    + {% for stage in stages %} +
    +
    +
    +
    + + +
    + +
    +
      + {% if perms.recruitment.add_candidate or request.user.employee_get in stage.stage_managers.all or request.user.employee_get in rec.recruitment_managers.all %} +
    • + Add Candidate +
    • + {% endif %} + {% if perms.recruitment.change_stage or request.user|recruitment_manages:rec or request.user.employee_get in stage.stage_managers.all %} +
    • + Edit +
    • +
    • + + {% trans "Bulk mail" %} + +
    • + {% endif %} + {% if perms.recruitment.delete_stage %} + + {% endif %} + + +
    +
    +
    +
    +
    +
    +
    + {% endfor %} + +
    + \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/actions.html b/recruitment/templates/cbv/recruitment/actions.html new file mode 100644 index 000000000..e79ebb76d --- /dev/null +++ b/recruitment/templates/cbv/recruitment/actions.html @@ -0,0 +1,31 @@ +{% load i18n %} +
    + + + + {% if perms.recruitment.change_recruitment %} + + {% endif %} + + {% if perms.recruitment.delete_recruitment %} + {% if instance.is_active %} + + {% else %} + + {% endif %} + {% endif %} + {% if perms.recruitment.delete_recruitment %} + + + + + + {% endif %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/detail_action.html b/recruitment/templates/cbv/recruitment/detail_action.html new file mode 100644 index 000000000..774de8102 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/detail_action.html @@ -0,0 +1,69 @@ +{% load i18n %} +
    + + {% if perms.recruitment.change_recruitment %} + + + {% endif %} + {% if perms.recruitment.delete_recruitment %} + {% if instance.is_active %} + + + {% else %} + + + {% endif %} + {% endif %} + {% if perms.recruitment.delete_recruitment %} + +
    + {% csrf_token %} + +
    + + {% endif %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/filters.html b/recruitment/templates/cbv/recruitment/filters.html new file mode 100644 index 000000000..2ecab32b0 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/filters.html @@ -0,0 +1,70 @@ +{% load i18n %}{% load static %} +
    +
    +
    {% trans "Recruitment" %}
    +
    +
    +
    +
    + + {{form.recruitment_managers}} +
    +
    + + {{form.start_date}} +
    +
    + + {% comment %} {{form.closed}} {% endcomment %} + +
    +
    +
    +
    + + {{form.company_id}} +
    +
    + + {{form.end_date}} +
    +
    + + {{form.is_published}} +
    +
    +
    +
    +
    + + +
    +
    {% trans "Advanced" %}
    +
    +
    +
    +
    + + {{form.start_from}} +
    +
    + + {{form.is_active}} +
    +
    +
    +
    + + {{form.end_till}} +
    +
    +
    +
    +
    +
    + + diff --git a/recruitment/templates/cbv/recruitment/managers_col.html b/recruitment/templates/cbv/recruitment/managers_col.html new file mode 100644 index 000000000..e16f56115 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/managers_col.html @@ -0,0 +1,37 @@ +{% load i18n %} + + +
    + {% for manager in instance.recruitment_managers.all %} + +
    +
    + Baby C. +
    + {{manager.employee_first_name|truncatechars:4}} +
    + + + +
    + {% endfor %} + {{instance.recruitment_managers.all|length}} {% trans "Managers" %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/open_jobs.html b/recruitment/templates/cbv/recruitment/open_jobs.html new file mode 100644 index 000000000..c8f94a93c --- /dev/null +++ b/recruitment/templates/cbv/recruitment/open_jobs.html @@ -0,0 +1,21 @@ +{% load i18n %} +
    + {% for jb in instance.open_positions.all %} + +
    +
    + Baby C. +
    + {{jb.job_position|truncatechars:5}}. +
    +
    + {% endfor %} + {{instance.open_positions.all|length}} {% trans "Jobs" %} + +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/rec_main.html b/recruitment/templates/cbv/recruitment/rec_main.html new file mode 100644 index 000000000..1882f78a0 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/rec_main.html @@ -0,0 +1,16 @@ + +
    + {% include "generic/horilla_list_table.html" %} +
    + + diff --git a/recruitment/templates/cbv/recruitment/recruitment.html b/recruitment/templates/cbv/recruitment/recruitment.html new file mode 100644 index 000000000..7ceffa098 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/recruitment.html @@ -0,0 +1,118 @@ +{% extends "index.html" %} + +{% load i18n %}{% load static recruitmentfilters %} + + +{% block content %} + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/recruitment_col.html b/recruitment/templates/cbv/recruitment/recruitment_col.html new file mode 100644 index 000000000..c88c394af --- /dev/null +++ b/recruitment/templates/cbv/recruitment/recruitment_col.html @@ -0,0 +1,26 @@ +{% load i18n %} + + + +
    +
    + + {{instance}} + {% if instance.linkedin_account_id %} +
    + +
    + {% endif %} +
    +
    + + + + + + + + \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/recruitment_form.html b/recruitment/templates/cbv/recruitment/recruitment_form.html new file mode 100644 index 000000000..17a780bad --- /dev/null +++ b/recruitment/templates/cbv/recruitment/recruitment_form.html @@ -0,0 +1,8 @@ +{% include "generic/horilla_form.html" %} + \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment/total_hires.html b/recruitment/templates/cbv/recruitment/total_hires.html new file mode 100644 index 000000000..3ca2c2c41 --- /dev/null +++ b/recruitment/templates/cbv/recruitment/total_hires.html @@ -0,0 +1,4 @@ +{% load i18n %} +
    + {{instance.total_hires}} {% trans "Hired" %} {% trans "of" %} {{instance.candidate.all|length}} {% trans "Candidates" %} +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/recruitment_survey/survey_form.html b/recruitment/templates/cbv/recruitment_survey/survey_form.html new file mode 100644 index 000000000..bb25e8ebf --- /dev/null +++ b/recruitment/templates/cbv/recruitment_survey/survey_form.html @@ -0,0 +1,83 @@ +{% load i18n %} +
    + {% include "generic/horilla_form.html" %} + +
    + + + \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/actions.html b/recruitment/templates/cbv/stages/actions.html new file mode 100644 index 000000000..36c84e458 --- /dev/null +++ b/recruitment/templates/cbv/stages/actions.html @@ -0,0 +1,43 @@ +{% load i18n %} +
    +
    + {% if perms.recruitment.change_stage %} + + + + {% endif %} + {% if perms.recruitment.change_stage %} + + + + {% endif %} + {% if perms.recruitment.delete_stage %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/detail_action.html b/recruitment/templates/cbv/stages/detail_action.html new file mode 100644 index 000000000..6e274b9e4 --- /dev/null +++ b/recruitment/templates/cbv/stages/detail_action.html @@ -0,0 +1,31 @@ +{% load i18n %} +
    + {% if perms.recruitment.change_stage %} + + + + + {% endif %} + {% if perms.recruitment.delete_stage %} +
    + {% csrf_token %} + +
    + {% endif %} +
    diff --git a/recruitment/templates/cbv/stages/filter.html b/recruitment/templates/cbv/stages/filter.html new file mode 100644 index 000000000..e1b7991b9 --- /dev/null +++ b/recruitment/templates/cbv/stages/filter.html @@ -0,0 +1,47 @@ +{% load i18n %}{% load static %} +
    +
    +
    {% trans "Stage" %}
    +
    +
    +
    +
    + + {{form.recruitment_id__recruitment_managers}} +
    +
    + + {{form.recruitment_id}} +
    +
    + + {{form.recruitment_id__job_position_id__department_id}} +
    +
    + + {{form.stage_type}} +
    + +
    + +
    +
    + + {{form.stage_managers}} +
    +
    + + {{form.recruitment_id__company_id}} +
    +
    + + {{form.recruitment_id__job_position_id}} +
    + + +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/managers.html b/recruitment/templates/cbv/stages/managers.html new file mode 100644 index 000000000..24302bd04 --- /dev/null +++ b/recruitment/templates/cbv/stages/managers.html @@ -0,0 +1,30 @@ +{% load i18n %} + {% for manager in instance.stage_managers.all %} + +
    +
    + Baby C. +
    + + {{manager.employee_first_name|truncatechars:6}}. + +
    + +
    + {% endfor %} + + {{instance.stage_managers.all|length}} {% trans "Managers"%} + + \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/stage_main.html b/recruitment/templates/cbv/stages/stage_main.html new file mode 100644 index 000000000..6f514cdf6 --- /dev/null +++ b/recruitment/templates/cbv/stages/stage_main.html @@ -0,0 +1,13 @@ +{% load i18n %} +
    + {% include "generic/horilla_nav.html" %} +
    + + \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/stages.html b/recruitment/templates/cbv/stages/stages.html new file mode 100644 index 000000000..642a3df1e --- /dev/null +++ b/recruitment/templates/cbv/stages/stages.html @@ -0,0 +1,97 @@ +{% extends "index.html" %} + +{% load i18n %}{% load static recruitmentfilters %} + + +{% block content %} + + + + + + + + +
    +
    +{% comment %} my_app/templates/my_app/generic/index.html {% endcomment %} + + + +{% include "generic/components.html" %} + + + + + +
    +
    +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/recruitment/templates/cbv/stages/title.html b/recruitment/templates/cbv/stages/title.html new file mode 100644 index 000000000..97d6d27d5 --- /dev/null +++ b/recruitment/templates/cbv/stages/title.html @@ -0,0 +1,33 @@ +{% load i18n %} + + +
    +
    + + {{instance}} +
    +
    + + + \ No newline at end of file diff --git a/recruitment/templates/dashboard/dashboard.html b/recruitment/templates/dashboard/dashboard.html index 44a0f7e82..40f2ba56e 100644 --- a/recruitment/templates/dashboard/dashboard.html +++ b/recruitment/templates/dashboard/dashboard.html @@ -8,7 +8,7 @@ } .scheduled-task{ background-color: #00afff2e !important; - } + }prett .ongoing-task{ background-color: #e6ff002e !important; } @@ -29,6 +29,15 @@ } +
    @@ -120,7 +129,10 @@ {% trans "Skill Zone Status" %}
    - {% if skill_zone %} +
    +
    + + {% comment %} {% if skill_zone %}
      {% for skill in skill_zone %}
    • @@ -146,7 +158,7 @@

      {% trans "No skill zone available." %}

    - {% endif %} + {% endif %} {% endcomment %}
    @@ -208,7 +220,10 @@ {% if onboarding_count %}{% trans "View" %}{% endif %}
    - {% if onboarding_count %} + +
    +
    + {% comment %} {% if onboarding_count %}
    - {% endif %} + {% endif %} {% endcomment %} {% endif %} @@ -269,12 +284,14 @@ {% if ongoing_recruitments %} -
    +
    {% trans "Current Hiring Pipeline" %}
    -
    +
    +
    + {% comment %}
    @@ -302,7 +319,7 @@ {% endfor %}
    -
    +
    {% endcomment %}
    {% endif %}
    @@ -367,7 +384,10 @@
    {% trans "Ongoing Recruitments & Hiring Managers" %}
    -
    + +
    +
    + {% comment %}
    @@ -387,7 +407,7 @@ {% endfor %}
    -
    +
    {% endcomment %}
    {% endif %}
    diff --git a/recruitment/templates/pipeline/components/candidate_stage_component.html b/recruitment/templates/pipeline/components/candidate_stage_component.html index b0991f2da..be5e33a98 100644 --- a/recruitment/templates/pipeline/components/candidate_stage_component.html +++ b/recruitment/templates/pipeline/components/candidate_stage_component.html @@ -1,5 +1,5 @@ +{% include "generic/components.html" %} {% load i18n recruitmentfilters horillafilters %} - {% if messages %}
    @@ -180,7 +180,7 @@ {% if candidate_ratings|has_candidate_rating:cand %}
    {% csrf_token %} -
    +
    {% for i in "54321" %} {% if perms.recruitment.add_interviewschedule or request.user.employee_get in stage.stage_managers.all %} diff --git a/recruitment/templates/pipeline/components/stages_tab_content.html b/recruitment/templates/pipeline/components/stages_tab_content.html index 86f226847..db192045a 100644 --- a/recruitment/templates/pipeline/components/stages_tab_content.html +++ b/recruitment/templates/pipeline/components/stages_tab_content.html @@ -34,8 +34,8 @@ justify-content: center; width: 50px; height: 28px;" class="oh-btn oh-btn--secondary-outline float-end ms-3" - hx-get="{% url 'add-candidate-to-stage' %}?stage_id={{stage.id}}" hx-target="#createTarget" - data-toggle="oh-modal-toggle" data-target="#createModal" title="Add Candidate"> + hx-get="{% url 'add-candidate-to-stage' %}?stage_id={{stage.id}}" hx-target="#genericModalBody" + data-toggle="oh-modal-toggle" data-target="#genericModal" title="Add Candidate"> {% endif %} diff --git a/recruitment/templates/pipeline/kanban_components/candidate_kanban_components.html b/recruitment/templates/pipeline/kanban_components/candidate_kanban_components.html index 62bce0642..9c175faa7 100644 --- a/recruitment/templates/pipeline/kanban_components/candidate_kanban_components.html +++ b/recruitment/templates/pipeline/kanban_components/candidate_kanban_components.html @@ -1,4 +1,5 @@ {% load i18n static recruitmentfilters horillafilters %} +{% include "generic/components.html" %} {% for cand in candidates %}
    {% trans "Schedule Interview" %}
  • {% endif %} @@ -131,9 +132,9 @@ {% trans "Add to Skill Zone" %} diff --git a/recruitment/templates/pipeline/pipeline.html b/recruitment/templates/pipeline/pipeline.html index b39953c00..6ea97d297 100644 --- a/recruitment/templates/pipeline/pipeline.html +++ b/recruitment/templates/pipeline/pipeline.html @@ -2,6 +2,7 @@
    +{% include "generic/components.html" %} - @@ -207,7 +754,28 @@ + {% endif %} + + +
    + +
    +
    +
    + + {% if "leave"|app_installed %} + {% if perms.leave.view_leaverequest or request.user|is_reportingmanager %} +
    +
    + {% trans "On Leave" %} +
    +
    +
    +
    + {% comment %}
    {% endcomment %} +
    {% endif %} + {% endif %}
    diff --git a/templates/filter_country.js b/templates/filter_country.js new file mode 100644 index 000000000..83cee0f0d --- /dev/null +++ b/templates/filter_country.js @@ -0,0 +1,333 @@ +// Countries +var country_arr = new Array("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 +var s_a = new Array(); +s_a[0] = ""; +s_a[1] = "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[2] = "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[3] = "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[4] = "Eastern|Manu'a|Rose Island|Swains Island|Western"; +s_a[5] = "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[6] = "Anguilla"; +s_a[7] = "Antartica"; +s_a[8] = "Barbuda|Redonda|Saint George|Saint John|Saint Mary|Saint Paul|Saint Peter|Saint Philip"; +s_a[9] = "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[10] = "Aragatsotn|Ararat|Armavir|Geghark'unik'|Kotayk'|Lorri|Shirak|Syunik'|Tavush|Vayots' Dzor|Yerevan"; +s_a[11] = "Aruba"; +s_a[12] = "Ashmore and Cartier Island"; +s_a[13] = "Australian Capital Territory|New South Wales|Northern Territory|Queensland|South Australia|Tasmania|Victoria|Western Australia"; +s_a[14] = "Burgenland|Kaernten|Niederoesterreich|Oberoesterreich|Salzburg|Steiermark|Tirol|Vorarlberg|Wien"; +s_a[15] = "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[16] = "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[17] = "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[18] = "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[19] = "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[20] = "Brestskaya (Brest)|Homyel'skaya (Homyel')|Horad Minsk|Hrodzyenskaya (Hrodna)|Mahilyowskaya (Mahilyow)|Minskaya|Vitsyebskaya (Vitsyebsk)"; +s_a[21] = "Antwerpen|Brabant Wallon|Brussels Capitol Region|Hainaut|Liege|Limburg|Luxembourg|Namur|Oost-Vlaanderen|Vlaams Brabant|West-Vlaanderen"; +s_a[22] = "Belize|Cayo|Corozal|Orange Walk|Stann Creek|Toledo"; +s_a[23] = "Alibori|Atakora|Atlantique|Borgou|Collines|Couffo|Donga|Littoral|Mono|Oueme|Plateau|Zou"; +s_a[24] = "Devonshire|Hamilton|Hamilton|Paget|Pembroke|Saint George|Saint Georges|Sandys|Smiths|Southampton|Warwick"; +s_a[25] = "Bumthang|Chhukha|Chirang|Daga|Geylegphug|Ha|Lhuntshi|Mongar|Paro|Pemagatsel|Punakha|Samchi|Samdrup Jongkhar|Shemgang|Tashigang|Thimphu|Tongsa|Wangdi Phodrang"; +s_a[26] = "Beni|Chuquisaca|Cochabamba|La Paz|Oruro|Pando|Potosi|Santa Cruz|Tarija"; +s_a[27] = "Federation of Bosnia and Herzegovina|Republika Srpska"; +s_a[28] = "Central|Chobe|Francistown|Gaborone|Ghanzi|Kgalagadi|Kgatleng|Kweneng|Lobatse|Ngamiland|North-East|Selebi-Pikwe|South-East|Southern"; +s_a[29] = "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[30] = "Anegada|Jost Van Dyke|Tortola|Virgin Gorda"; +s_a[31] = "Belait|Brunei and Muara|Temburong|Tutong"; +s_a[32] = "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[33] = "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[34] = "Ayeyarwady|Bago|Chin State|Kachin State|Kayah State|Kayin State|Magway|Mandalay|Mon State|Rakhine State|Sagaing|Shan State|Tanintharyi|Yangon"; +s_a[35] = "Bubanza|Bujumbura|Bururi|Cankuzo|Cibitoke|Gitega|Karuzi|Kayanza|Kirundo|Makamba|Muramvya|Muyinga|Mwaro|Ngozi|Rutana|Ruyigi"; +s_a[36] = "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[37] = "Adamaoua|Centre|Est|Extreme-Nord|Littoral|Nord|Nord-Ouest|Ouest|Sud|Sud-Ouest"; +s_a[38] = "Alberta|British Columbia|Manitoba|New Brunswick|Newfoundland|Northwest Territories|Nova Scotia|Nunavut|Ontario|Prince Edward Island|Quebec|Saskatchewan|Yukon Territory"; +s_a[39] = "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[40] = "Creek|Eastern|Midland|South Town|Spot Bay|Stake Bay|West End|Western"; +s_a[41] = "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[42] = "Batha|Biltine|Borkou-Ennedi-Tibesti|Chari-Baguirmi|Guera|Kanem|Lac|Logone Occidental|Logone Oriental|Mayo-Kebbi|Moyen-Chari|Ouaddai|Salamat|Tandjile"; +s_a[43] = "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[44] = "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[45] = "Christmas Island"; +s_a[46] = "Clipperton Island"; +s_a[47] = "Direction Island|Home Island|Horsburgh Island|North Keeling Island|South Island|West Island"; +s_a[48] = "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[49] = "Anjouan (Nzwani)|Domoni|Fomboni|Grande Comore (Njazidja)|Moheli (Mwali)|Moroni|Moutsamoudou"; +s_a[50] = "Bandundu|Bas-Congo|Equateur|Kasai-Occidental|Kasai-Oriental|Katanga|Kinshasa|Maniema|Nord-Kivu|Orientale|Sud-Kivu"; +s_a[51] = "Bouenza|Brazzaville|Cuvette|Kouilou|Lekoumou|Likouala|Niari|Plateaux|Pool|Sangha"; +s_a[52] = "Aitutaki|Atiu|Avarua|Mangaia|Manihiki|Manuae|Mauke|Mitiaro|Nassau Island|Palmerston|Penrhyn|Pukapuka|Rakahanga|Rarotonga|Suwarrow|Takutea"; +s_a[53] = "Alajuela|Cartago|Guanacaste|Heredia|Limon|Puntarenas|San Jose"; +s_a[54] = "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[55] = "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[56] = "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[57] = "Famagusta|Kyrenia|Larnaca|Limassol|Nicosia|Paphos"; +s_a[58] = "Brnensky|Budejovicky|Jihlavsky|Karlovarsky|Kralovehradecky|Liberecky|Olomoucky|Ostravsky|Pardubicky|Plzensky|Praha|Stredocesky|Ustecky|Zlinsky"; +s_a[59] = "Arhus|Bornholm|Fredericksberg|Frederiksborg|Fyn|Kobenhavn|Kobenhavns|Nordjylland|Ribe|Ringkobing|Roskilde|Sonderjylland|Storstrom|Vejle|Vestsjalland|Viborg"; +s_a[60] = "'Ali Sabih|Dikhil|Djibouti|Obock|Tadjoura"; +s_a[61] = "Saint Andrew|Saint David|Saint George|Saint John|Saint Joseph|Saint Luke|Saint Mark|Saint Patrick|Saint Paul|Saint Peter"; +s_a[62] = "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[63] = "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[64] = "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[65] = "Ahuachapan|Cabanas|Chalatenango|Cuscatlan|La Libertad|La Paz|La Union|Morazan|San Miguel|San Salvador|San Vicente|Santa Ana|Sonsonate|Usulutan"; +s_a[66] = "Annobon|Bioko Norte|Bioko Sur|Centro Sur|Kie-Ntem|Litoral|Wele-Nzas"; +s_a[67] = "Akale Guzay|Barka|Denkel|Hamasen|Sahil|Semhar|Senhit|Seraye"; +s_a[68] = "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[69] = "Adis Abeba (Addis Ababa)|Afar|Amara|Dire Dawa|Gambela Hizboch|Hareri Hizb|Oromiya|Sumale|Tigray|YeDebub Biheroch Bihereseboch na Hizboch"; +s_a[70] = "Europa Island"; +s_a[71] = "Falkland Islands (Islas Malvinas)" +s_a[72] = "Bordoy|Eysturoy|Mykines|Sandoy|Skuvoy|Streymoy|Suduroy|Tvoroyri|Vagar"; +s_a[73] = "Central|Eastern|Northern|Rotuma|Western"; +s_a[74] = "Aland|Etela-Suomen Laani|Ita-Suomen Laani|Lansi-Suomen Laani|Lappi|Oulun Laani"; +s_a[75] = "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[76] = "French Guiana"; +s_a[77] = "Archipel des Marquises|Archipel des Tuamotu|Archipel des Tubuai|Iles du Vent|Iles Sous-le-Vent"; +s_a[78] = "Adelie Land|Ile Crozet|Iles Kerguelen|Iles Saint-Paul et Amsterdam"; +s_a[79] = "Estuaire|Haut-Ogooue|Moyen-Ogooue|Ngounie|Nyanga|Ogooue-Ivindo|Ogooue-Lolo|Ogooue-Maritime|Woleu-Ntem"; +s_a[80] = "Banjul|Central River|Lower River|North Bank|Upper River|Western"; +s_a[81] = "Gaza Strip"; +s_a[82] = "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[83] = "Baden-Wuerttemberg|Bayern|Berlin|Brandenburg|Bremen|Hamburg|Hessen|Mecklenburg-Vorpommern|Niedersachsen|Nordrhein-Westfalen|Rheinland-Pfalz|Saarland|Sachsen|Sachsen-Anhalt|Schleswig-Holstein|Thueringen"; +s_a[84] = "Ashanti|Brong-Ahafo|Central|Eastern|Greater Accra|Northern|Upper East|Upper West|Volta|Western"; +s_a[85] = "Gibraltar"; +s_a[86] = "Ile du Lys|Ile Glorieuse"; +s_a[87] = "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[88] = "Avannaa (Nordgronland)|Kitaa (Vestgronland)|Tunu (Ostgronland)" +s_a[89] = "Carriacou and Petit Martinique|Saint Andrew|Saint David|Saint George|Saint John|Saint Mark|Saint Patrick"; +s_a[90] = "Basse-Terre|Grande-Terre|Iles de la Petite Terre|Iles des Saintes|Marie-Galante"; +s_a[91] = "Guam"; +s_a[92] = "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[93] = "Castel|Forest|St. Andrew|St. Martin|St. Peter Port|St. Pierre du Bois|St. Sampson|St. Saviour|Torteval|Vale"; +s_a[94] = "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[95] = "Bafata|Biombo|Bissau|Bolama-Bijagos|Cacheu|Gabu|Oio|Quinara|Tombali"; +s_a[96] = "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[97] = "Artibonite|Centre|Grand'Anse|Nord|Nord-Est|Nord-Ouest|Ouest|Sud|Sud-Est"; +s_a[98] = "Heard Island and McDonald Islands"; +s_a[99] = "Holy See (Vatican City)" +s_a[100] = "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[101] = "Hong Kong"; +s_a[102] = "Howland Island"; +s_a[103] = "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[104] = "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[105] = "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|Tripura|Uttar Pradesh|Uttaranchal|West Bengal"; +s_a[106] = "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[107] = "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[108] = "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[109] = "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[110] = "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[111] = "Central|Haifa|Jerusalem|Northern|Southern|Tel Aviv"; +s_a[112] = "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[113] = "Clarendon|Hanover|Kingston|Manchester|Portland|Saint Andrew|Saint Ann|Saint Catherine|Saint Elizabeth|Saint James|Saint Mary|Saint Thomas|Trelawny|Westmoreland"; +s_a[114] = "Jan Mayen"; +s_a[115] = "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[116] = "Jarvis Island"; +s_a[117] = "Jersey"; +s_a[118] = "Johnston Atoll"; +s_a[119] = "'Amman|Ajlun|Al 'Aqabah|Al Balqa'|Al Karak|Al Mafraq|At Tafilah|Az Zarqa'|Irbid|Jarash|Ma'an|Madaba"; +s_a[120] = "Juan de Nova Island"; +s_a[121] = "Almaty|Aqmola|Aqtobe|Astana|Atyrau|Batys Qazaqstan|Bayqongyr|Mangghystau|Ongtustik Qazaqstan|Pavlodar|Qaraghandy|Qostanay|Qyzylorda|Shyghys Qazaqstan|Soltustik Qazaqstan|Zhambyl"; +s_a[122] = "Central|Coast|Eastern|Nairobi Area|North Eastern|Nyanza|Rift Valley|Western"; +s_a[123] = "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[124] = "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[125] = "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[126] = "Al 'Asimah|Al Ahmadi|Al Farwaniyah|Al Jahra'|Hawalli"; +s_a[127] = "Batken Oblasty|Bishkek Shaary|Chuy Oblasty (Bishkek)|Jalal-Abad Oblasty|Naryn Oblasty|Osh Oblasty|Talas Oblasty|Ysyk-Kol Oblasty (Karakol)" +s_a[128] = "Attapu|Bokeo|Bolikhamxai|Champasak|Houaphan|Khammouan|Louangnamtha|Louangphabang|Oudomxai|Phongsali|Salavan|Savannakhet|Viangchan|Viangchan|Xaignabouli|Xaisomboun|Xekong|Xiangkhoang"; +s_a[129] = "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[130] = "Beyrouth|Ech Chimal|Ej Jnoub|El Bekaa|Jabal Loubnane"; +s_a[131] = "Berea|Butha-Buthe|Leribe|Mafeteng|Maseru|Mohales Hoek|Mokhotlong|Qacha's Nek|Quthing|Thaba-Tseka"; +s_a[132] = "Bomi|Bong|Grand Bassa|Grand Cape Mount|Grand Gedeh|Grand Kru|Lofa|Margibi|Maryland|Montserrado|Nimba|River Cess|Sinoe"; +s_a[133] = "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[134] = "Balzers|Eschen|Gamprin|Mauren|Planken|Ruggell|Schaan|Schellenberg|Triesen|Triesenberg|Vaduz"; +s_a[135] = "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[136] = "Diekirch|Grevenmacher|Luxembourg"; +s_a[137] = "Macau"; +s_a[138] = "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[139] = "Antananarivo|Antsiranana|Fianarantsoa|Mahajanga|Toamasina|Toliara"; +s_a[140] = "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[141] = "Johor|Kedah|Kelantan|Labuan|Melaka|Negeri Sembilan|Pahang|Perak|Perlis|Pulau Pinang|Sabah|Sarawak|Selangor|Terengganu|Wilayah Persekutuan"; +s_a[142] = "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[143] = "Gao|Kayes|Kidal|Koulikoro|Mopti|Segou|Sikasso|Tombouctou"; +s_a[144] = "Valletta"; +s_a[145] = "Man, Isle of"; +s_a[146] = "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[147] = "Martinique"; +s_a[148] = "Adrar|Assaba|Brakna|Dakhlet Nouadhibou|Gorgol|Guidimaka|Hodh Ech Chargui|Hodh El Gharbi|Inchiri|Nouakchott|Tagant|Tiris Zemmour|Trarza"; +s_a[149] = "Agalega Islands|Black River|Cargados Carajos Shoals|Flacq|Grand Port|Moka|Pamplemousses|Plaines Wilhems|Port Louis|Riviere du Rempart|Rodrigues|Savanne"; +s_a[150] = "Mayotte"; +s_a[151] = "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[152] = "Chuuk (Truk)|Kosrae|Pohnpei|Yap"; +s_a[153] = "Midway Islands"; +s_a[154] = "Balti|Cahul|Chisinau|Chisinau|Dubasari|Edinet|Gagauzia|Lapusna|Orhei|Soroca|Tighina|Ungheni"; +s_a[155] = "Fontvieille|La Condamine|Monaco-Ville|Monte-Carlo"; +s_a[156] = "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[157] = "Saint Anthony|Saint Georges|Saint Peter's"; +s_a[158] = "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[159] = "Cabo Delgado|Gaza|Inhambane|Manica|Maputo|Nampula|Niassa|Sofala|Tete|Zambezia"; +s_a[160] = "Caprivi|Erongo|Hardap|Karas|Khomas|Kunene|Ohangwena|Okavango|Omaheke|Omusati|Oshana|Oshikoto|Otjozondjupa"; +s_a[161] = "Aiwo|Anabar|Anetan|Anibare|Baiti|Boe|Buada|Denigomodu|Ewa|Ijuw|Meneng|Nibok|Uaboe|Yaren"; +s_a[162] = "Bagmati|Bheri|Dhawalagiri|Gandaki|Janakpur|Karnali|Kosi|Lumbini|Mahakali|Mechi|Narayani|Rapti|Sagarmatha|Seti"; +s_a[163] = "Drenthe|Flevoland|Friesland|Gelderland|Groningen|Limburg|Noord-Brabant|Noord-Holland|Overijssel|Utrecht|Zeeland|Zuid-Holland"; +s_a[164] = "Netherlands Antilles"; +s_a[165] = "Iles Loyaute|Nord|Sud"; +s_a[166] = "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[167] = "Atlantico Norte|Atlantico Sur|Boaco|Carazo|Chinandega|Chontales|Esteli|Granada|Jinotega|Leon|Madriz|Managua|Masaya|Matagalpa|Nueva Segovia|Rio San Juan|Rivas"; +s_a[168] = "Agadez|Diffa|Dosso|Maradi|Niamey|Tahoua|Tillaberi|Zinder"; +s_a[169] = "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[170] = "Niue"; +s_a[171] = "Norfolk Island"; +s_a[172] = "Northern Islands|Rota|Saipan|Tinian"; +s_a[173] = "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[174] = "Ad Dakhiliyah|Al Batinah|Al Wusta|Ash Sharqiyah|Az Zahirah|Masqat|Musandam|Zufar"; +s_a[175] = "Balochistan|Federally Administered Tribal Areas|Islamabad Capital Territory|North-West Frontier Province|Punjab|Sindh"; +s_a[176] = "Aimeliik|Airai|Angaur|Hatobohei|Kayangel|Koror|Melekeok|Ngaraard|Ngarchelong|Ngardmau|Ngatpang|Ngchesar|Ngeremlengui|Ngiwal|Palau Island|Peleliu|Sonsoral|Tobi"; +s_a[177] = "Bocas del Toro|Chiriqui|Cocle|Colon|Darien|Herrera|Los Santos|Panama|San Blas|Veraguas"; +s_a[178] = "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[179] = "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[180] = "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[181] = "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[182] = "Pitcaim Islands"; +s_a[183] = "Dolnoslaskie|Kujawsko-Pomorskie|Lodzkie|Lubelskie|Lubuskie|Malopolskie|Mazowieckie|Opolskie|Podkarpackie|Podlaskie|Pomorskie|Slaskie|Swietokrzyskie|Warminsko-Mazurskie|Wielkopolskie|Zachodniopomorskie"; +s_a[184] = "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[185] = "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[186] = "Ad Dawhah|Al Ghuwayriyah|Al Jumayliyah|Al Khawr|Al Wakrah|Ar Rayyan|Jarayan al Batinah|Madinat ash Shamal|Umm Salal"; +s_a[187] = "Reunion"; +s_a[188] = "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[189] = "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[190] = "Butare|Byumba|Cyangugu|Gikongoro|Gisenyi|Gitarama|Kibungo|Kibuye|Kigali Rurale|Kigali-ville|Ruhengeri|Umutara"; +s_a[191] = "Ascension|Saint Helena|Tristan da Cunha"; +s_a[192] = "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[193] = "Anse-la-Raye|Castries|Choiseul|Dauphin|Dennery|Gros Islet|Laborie|Micoud|Praslin|Soufriere|Vieux Fort"; +s_a[194] = "Miquelon|Saint Pierre"; +s_a[195] = "Charlotte|Grenadines|Saint Andrew|Saint David|Saint George|Saint Patrick"; +s_a[196] = "A'ana|Aiga-i-le-Tai|Atua|Fa'asaleleaga|Gaga'emauga|Gagaifomauga|Palauli|Satupa'itea|Tuamasaga|Va'a-o-Fonoti|Vaisigano"; +s_a[197] = "Acquaviva|Borgo Maggiore|Chiesanuova|Domagnano|Faetano|Fiorentino|Monte Giardino|San Marino|Serravalle"; +s_a[198] = "Principe|Sao Tome"; +s_a[199] = "'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[200] = "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[201] = "Dakar|Diourbel|Fatick|Kaolack|Kolda|Louga|Saint-Louis|Tambacounda|Thies|Ziguinchor"; +s_a[202] = "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[203] = "Eastern|Northern|Southern|Western"; +s_a[204] = "Singapore"; +s_a[205] = "Banskobystricky|Bratislavsky|Kosicky|Nitriansky|Presovsky|Trenciansky|Trnavsky|Zilinsky"; +s_a[206] = "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[207] = "Bellona|Central|Choiseul (Lauru)|Guadalcanal|Honiara|Isabel|Makira|Malaita|Rennell|Temotu|Western"; +s_a[208] = "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[209] = "Eastern Cape|Free State|Gauteng|KwaZulu-Natal|Mpumalanga|North-West|Northern Cape|Northern Province|Western Cape"; +s_a[210] = "Bird Island|Bristol Island|Clerke Rocks|Montagu Island|Saunders Island|South Georgia|Southern Thule|Traversay Islands"; +s_a[211] = "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[212] = "Spratly Islands"; +s_a[213] = "Central|Eastern|North Central|North Eastern|North Western|Northern|Sabaragamuwa|Southern|Uva|Western"; +s_a[214] = "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[215] = "Brokopondo|Commewijne|Coronie|Marowijne|Nickerie|Para|Paramaribo|Saramacca|Sipaliwini|Wanica"; +s_a[216] = "Barentsoya|Bjornoya|Edgeoya|Hopen|Kvitoya|Nordaustandet|Prins Karls Forland|Spitsbergen"; +s_a[217] = "Hhohho|Lubombo|Manzini|Shiselweni"; +s_a[218] = "Blekinge|Dalarnas|Gavleborgs|Gotlands|Hallands|Jamtlands|Jonkopings|Kalmar|Kronobergs|Norrbottens|Orebro|Ostergotlands|Skane|Sodermanlands|Stockholms|Uppsala|Varmlands|Vasterbottens|Vasternorrlands|Vastmanlands|Vastra Gotalands"; +s_a[219] = "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[220] = "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[221] = "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[222] = "Viloyati Khatlon|Viloyati Leninobod|Viloyati Mukhtori Kuhistoni Badakhshon"; +s_a[223] = "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[224] = "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[225] = "Tobago"; +s_a[226] = "De La Kara|Des Plateaux|Des Savanes|Du Centre|Maritime"; +s_a[227] = "Atafu|Fakaofo|Nukunonu"; +s_a[228] = "Ha'apai|Tongatapu|Vava'u"; +s_a[229] = "Arima|Caroni|Mayaro|Nariva|Port-of-Spain|Saint Andrew|Saint David|Saint George|Saint Patrick|San Fernando|Victoria"; +s_a[230] = "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[231] = "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[232] = "Ahal Welayaty|Balkan Welayaty|Dashhowuz Welayaty|Lebap Welayaty|Mary Welayaty"; +s_a[233] = "Tuvalu"; +s_a[234] = "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[235] = "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[236] = "'Ajman|Abu Zaby (Abu Dhabi)|Al Fujayrah|Ash Shariqah (Sharjah)|Dubayy (Dubai)|Ra's al Khaymah|Umm al Qaywayn"; +s_a[237] = "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[238] = "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[239] = "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[240] = "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[241] = "Malampa|Penama|Sanma|Shefa|Tafea|Torba"; +s_a[242] = "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[243] = "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[244] = "Saint Croix|Saint John|Saint Thomas"; +s_a[245] = "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[246] = "Alo|Sigave|Wallis"; +s_a[247] = "West Bank"; +s_a[248] = "Western Sahara"; +s_a[249] = "'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[250] = "Kosovo|Montenegro|Serbia|Vojvodina"; +s_a[251] = "Central|Copperbelt|Eastern|Luapula|Lusaka|North-Western|Northern|Southern|Western"; +s_a[252] = "Bulawayo|Harare|ManicalandMashonaland Central|Mashonaland East|Mashonaland West|Masvingo|Matabeleland North|Matabeleland South|Midlands"; + + +function populateStates(countryElementId, stateElementId) { + var countryElement = document.getElementById(countryElementId); + if (!countryElement) { + console.error(`Element with ID '${countryElementId}' not found.`); + return; // Exit if the country element doesn't exist + } + + var selectedCountryIndex = countryElement.selectedIndex; + + var stateElement = document.getElementById(stateElementId); + if (!stateElement) { + console.error(`Element with ID '${stateElementId}' not found.`); + return; // Exit if the state element doesn't exist + } + + stateElement.length = 0; // Clear existing options + stateElement.options[0] = new Option('Select State', ''); + stateElement.selectedIndex = 0; + + if (s_a[selectedCountryIndex]) { + var state_arr = s_a[selectedCountryIndex].split("|"); + for (var i = 0; i < state_arr.length; i++) { + stateElement.options[stateElement.length] = new Option(state_arr[i], state_arr[i]); + } + } +} + +function populateCountries(countryElementId, stateElementId) { + + // Get the country select element by its ID + var countryElement = document.getElementById(countryElementId); + + // Check if the countryElement exists + if (!countryElement) { + console.error(`Element with ID ${countryElementId} not found`); + return; // Exit the function if the element does not exist + } + + // Clear the existing options + countryElement.length = 0; + + // Add default option + countryElement.options[0] = new Option('Select country',''); + countryElement.selectedIndex = 0; + + // Populate with countries + for (var i = 0; i < country_arr.length; i++) { + countryElement.options[countryElement.length] = new Option(country_arr[i], country_arr[i]); + } + + // Handle the selected value + var value = countryElement.value; + if (value !== undefined) { + $(countryElement).append($('
    @@ -603,6 +639,35 @@
    {% endif %} + {% if "whatsapp"|app_installed %} + {% if perms.whatsapp.view_whatsappcredentials %} +
  • + +
  • +
    + {% endif %} + {% endif %} @@ -614,6 +679,18 @@ {% if not request.user.driverviewed_set.first or "settings" not in request.user.driverviewed_set.first.user_viewed %} diff --git a/whatsapp/__init__.py b/whatsapp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/whatsapp/admin.py b/whatsapp/admin.py new file mode 100644 index 000000000..2e287f6ba --- /dev/null +++ b/whatsapp/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from whatsapp.models import WhatsappCredientials, WhatsappFlowDetails + +# Register your models here. +admin.site.register(WhatsappCredientials) +admin.site.register(WhatsappFlowDetails) \ No newline at end of file diff --git a/whatsapp/apps.py b/whatsapp/apps.py new file mode 100644 index 000000000..89758820c --- /dev/null +++ b/whatsapp/apps.py @@ -0,0 +1,15 @@ +from django.apps import AppConfig + + +class WhatsappConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'whatsapp' + + def ready(self): + from django.urls import include, path + from horilla.urls import urlpatterns + + urlpatterns.append( + path("whatsapp/", include("whatsapp.urls")), + ) + super().ready() \ No newline at end of file diff --git a/whatsapp/cbv/whatsapp.py b/whatsapp/cbv/whatsapp.py new file mode 100644 index 000000000..01b34c1b3 --- /dev/null +++ b/whatsapp/cbv/whatsapp.py @@ -0,0 +1,146 @@ +from typing import Any +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from horilla_views.generic.cbv.views import ( + HorillaFormView, + HorillaListView, + HorillaNavView, +) +from whatsapp.filters import CredentialsViewFilter +from whatsapp.forms import WhatsappForm +from whatsapp.models import WhatsappCredientials +from django.utils.translation import gettext_lazy as _trans +from django.contrib import messages + +from whatsapp.utils import send_text_message + + +class CredentialListView(HorillaListView): + + model = WhatsappCredientials + filter_class = CredentialsViewFilter + show_filter_tags = False + + columns = [ + (_trans("Phone Number"), "meta_phone_number"), + (_trans("Phone Number ID"), "meta_phone_number_id"), + (_trans("Bussiness ID"), "meta_business_id"), + (_trans("Webhook Token"), "get_webhook_token"), + (_trans("Token"), "token_render"), + ] + # sortby_mapping = [("Bussiness ID", "meta_business_id")] + row_attrs = """ + id = "credential{get_instance}" + """ + option_method ="get_publish_button" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.search_url = reverse("whatsapp-credential-list") + self.view_id = "CredentialList" + self.actions = [ + { + "action": "Edit", + "icon": "create-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + data-toggle="oh-modal-toggle" + data-target = "#genericModal" + hx-target = "#genericModalBody" + hx-get = "{get_update_url}" + """, + }, + { + "action": "Test test message", + "icon": "link-outline", + "attrs": """ + class="oh-btn oh-btn--light-bkg w-100" + data-toggle="oh-modal-toggle" + data-target = "#genericModal" + hx-target = "#genericModalBody" + hx-get = "{get_test_message_url}" + """, + }, + { + "action": "Delete", + "icon": "trash-outline", + "attrs": """ + class="oh-btn oh-btn--danger-outline w-100" + hx-confirm = "Are you sure you want to delete this credential?" + hx-post = "{get_delete_url}" + hx-target = "#credential{get_instance}" + hx-swap = "outerHTML" + """, + }, + ] + + self.row_attrs = """ + id="credential{get_instance}" + {get_primary} + """ + + +class CredentialNav(HorillaNavView): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.search_url = reverse("whatsapp-credential-list") + self.create_attrs = f""" + data-toggle="oh-modal-toggle" + data-target = "#genericModal" + hx-target = "#genericModalBody" + hx-get = "{reverse('whatsapp-credential-create')}" + """ + + nav_title = "Whatsapp Credentials" + search_swap_target = "#listContainer" + filter_instance = CredentialsViewFilter() + + +class CredentialForm(HorillaFormView): + model = WhatsappCredientials + form_class = WhatsappForm + new_display_title = "Create whatsapp" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.form.instance.pk: + self.form_class.verbose_name = "Update Credentials" + return context + + def form_valid(self, form: WhatsappForm) -> HttpResponse: + if form.is_valid(): + if self.form.instance.pk: + messages.success(self.request, "Crediential updated successfully") + else: + messages.success(self.request, "Crediential created successfully") + form.save() + return self.HttpResponse() + return super().form_valid(form) + + +def delete_credentials(request): + id = request.GET.get("id") + crediential = WhatsappCredientials.objects.filter(id=id).first() + count = WhatsappCredientials.objects.count() + crediential.delete() + messages.success(request, f"Crediential deleted.") + if count == 1: + return HttpResponse("") + return HttpResponse("") + + +def send_test_message(request): + message = "This is a test message" + if request.method == "POST": + number = request.POST.get("number") + response = send_text_message(number, message) + print(response) + if response: + messages.success(request, "Message sent successfully") + else: + messages.error(request, "message not send") + return HttpResponse("") + + return render(request, "whatsapp/send_test_message_form.html") diff --git a/whatsapp/encryption.py b/whatsapp/encryption.py new file mode 100644 index 000000000..b5ac09fa7 --- /dev/null +++ b/whatsapp/encryption.py @@ -0,0 +1,281 @@ +import json +import base64 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + + +PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- +MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCitHXNMGC04pSI +di+ynQjLDQd1LrExd1nYMxdOFnX/CnVEJGVEUKPdDm5M300ULugeFPx76MsjQx97 +vWAgZDMEA2+WyKFAHqmgRvVoiESZZnduIj2akVjFZvA24sbFpGunaiMPq61kWUBR +0pFRw4pnXEbPQnqu9H5vEJ7W5+G6CDlJD3eeTTkM85/ch1/REJbSDyp+Tozrc0Wa +Mv5xMUbVtSjwxaqwSfdMbAfJxnaaZ5SKwgiUEIsLKsQISWX3+qq6USWOCbJs6oTV +eWgeZfxgOjCUovJsZ2TuJ4aNfMtI+dFm3OMaeB2ypa4F4lWu8pq6FVhTseQntfUu +fQv4P+iRAgMBAAECggEABGOp6ecsNLUIHMZTcxYZbqDjWp3v2c3GdraqIkko1cCK +eVQiBz3Frej9wMUlZy38xRL73Lvi/wiIiOYK+dS6K5mMIR04fGpXWSOQ60kB0MGa +5zW1Q744DttAD7r+ccaFwPZ0C7At9U8TFSIBGZuU2ET9BApfFOkzn/tqzZFj3Yjg +OgWaGCvtOGCjLgjN1CWRTq+U66SUuVEtm8cXX8o4hVGfy2ITdg10xW+88qgLqxLj +/2RKPTixjmlwp1/28Z0rotp4GFUU5yplDq86YkdYNF8wHIPx0NiEUiLmAtfZrpmM +0xyJVJbgWh8QASzOxM1lq8WOWOOhJnVnkowYJsxVgQKBgQDZisbd8n59rJQfDMbK +7/cQ0gedl7No+0taY30hSckeR73yxAvmz83jqiD6qOlbaCbb+Rx1PPKewRZybrQV +n0CdFJ5oB1lbtLY7ftIlGQx2MiI4e+lAiCn+Zo2FnIm+C6LJKWqR18vLlHKebbJK +IAHdW76roZsdqAyBSyn0fh2gXQKBgQC/d+/30lIBafNCc6oDXbZFrH9/sRqCEg1u +jOhgNjwYZ23IkBMghyhD3eAiymvUFYCQ2pUfSMqygBbFUay5h5/PF34vzJmSVwC0 +6SVemod/2Z7ZcVUogplSdAW1+V4f/3pyIptgKAAqlsE7lAQJAiyd7FnXx8/Rx7Cw +IVh+Jz11xQKBgBaTOTn1HT1LeH+UYtjSeDAtq46mHH8rfNFfe6/FqXJT/ZlA0P9d +1z7l+9AnUTgkIcw4GMTt0zu4S+0KIfQQd7MVXa7r/FDw+uxHp+UjqVBmuXhlG3qP +5tO4rr0L1pt7N6RqgN2rqEFzIUXhmlvo4GipSasj9SXpt4p/U1ZE9CwdAoGBAKSU +5jMyGMeaWT4Pyl5mWV1+r4IFrHGOLvmOKdk6BWI81cOHBMn7JANiX13IffOqH/9j +xLdFjObu76PhVwWLrTUITrGrv35pRvQ7TKILVtnxKHhk0Pyndj/H93i6x8vdgVVG +piR7fdkeCS+7RdSwh8Wf+oJfASaj7h8YKscV1+C5An9SKKO185jufNw+6cToVvBG +ii91044lvc8XidFgR6t3M9cJS3pHNemIfg2dZCu7i2/UbAIrsMDt4Uv4QGOaRGr/ +rZ7JoquIPXVTaUjSJLWOELUi9TDBB1F04duW/xNsKswrFIk+mT0jMO3zB/LyCT5m +BCyI7ou85astK8+e4Q12 +-----END PRIVATE KEY----- +""" +PASSPHRASE = "Horilla" +APP_SECRET = "6c18bf72d5f486479bf66c5807c2b393" + + +class FlowEndpointException(Exception): + def __init__(self, status_code, message): + super().__init__(message) + self.status_code = status_code + + +def decrypt_request(body, private_pem, passphrase): + # Extract encrypted data from the request body + encrypted_aes_key = body['encrypted_aes_key'] + encrypted_flow_data = body['encrypted_flow_data'] + initial_vector = body['initial_vector'] + + private_key = serialization.load_pem_private_key( + private_pem.encode(), + password=None, + backend=default_backend() + ) + + decrypted_aes_key = private_key.decrypt( + base64.b64decode(encrypted_aes_key), + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + flow_data_buffer = base64.b64decode(encrypted_flow_data) + iv_buffer = base64.b64decode(initial_vector) + + TAG_LENGTH = 16 + encrypted_flow_data_body = flow_data_buffer[:-TAG_LENGTH] + encrypted_flow_data_tag = flow_data_buffer[-TAG_LENGTH:] + + cipher = Cipher(algorithms.AES(decrypted_aes_key), modes.GCM(iv_buffer, encrypted_flow_data_tag), backend=default_backend()) + decryptor = cipher.decryptor() + + decrypted_json_string = decryptor.update(encrypted_flow_data_body) + decryptor.finalize() + + return { + 'decrypted_body': json.loads(decrypted_json_string.decode('utf-8')), + 'aes_key_buffer': decrypted_aes_key, + 'initial_vector_buffer': iv_buffer + } + +def encrypt_response(response, aes_key_buffer, initial_vector_buffer): + response_bytes = json.dumps(response).encode('utf-8') + + flipped_iv = bytearray((b ^ 0xFF) for b in initial_vector_buffer) + + cipher = Cipher( + algorithms.AES(aes_key_buffer), + modes.GCM(bytes(flipped_iv)), + backend=default_backend() + ) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(response_bytes) + encryptor.finalize() + ciphertext_with_tag = ciphertext + encryptor.tag + encrypted_message = base64.b64encode(ciphertext_with_tag).decode('utf-8') + + return encrypted_message + +# -------------------------------------------------views.py ------------------------------------------------ + +import base64 +import hashlib +import hmac + +import json +import os +from base64 import b64decode, b64encode +from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1, hashes +from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from django.http import HttpResponse +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt + +# Load the private key string +# PRIVATE_KEY = os.environ.get('PRIVATE_KEY') +# Example: +# '''-----BEGIN RSA PRIVATE KEY----- +# MIIE... +# ... +# ...AQAB +# -----END RSA PRIVATE KEY-----''' + +# PRIVATE_KEY = settings.PRIVATE_KEY + + +# @csrf_exempt +# def end_point(request): +# try: +# # Parse the request body +# body = json.loads(request.body) + +# # Read the request fields +# encrypted_flow_data_b64 = body["encrypted_flow_data"] +# encrypted_aes_key_b64 = body["encrypted_aes_key"] +# initial_vector_b64 = body["initial_vector"] + +# decrypted_data, aes_key, iv = decrypt_request( +# encrypted_flow_data_b64, encrypted_aes_key_b64, initial_vector_b64 +# ) + +# # Return the next screen & data to the client +# if decrypted_data["action"] == "ping": +# response = {"data": {"status": "active"}} + +# else: +# response = {"screen": "screen_one", "data": {"some_key": "some_value"}} + +# # Return the response as plaintext +# return HttpResponse( +# encrypt_response(response, aes_key, iv), content_type="text/plain" +# ) +# except Exception as e: +# print(e) +# return JsonResponse({}, status=500) + + +@csrf_exempt +def end_point(request): + try: + body = json.loads(request.body) + + encrypted_flow_data_b64 = body["encrypted_flow_data"] + encrypted_aes_key_b64 = body["encrypted_aes_key"] + initial_vector_b64 = body["initial_vector"] + + decrypted_data, aes_key, iv = decrypt_request( + encrypted_flow_data_b64, encrypted_aes_key_b64, initial_vector_b64 + ) + if decrypted_data["action"] == "ping": + response = {"data": {"status": "active"}} + else: + leave_types = get_leave_types() + + response = {"screen": "screen_one", "data": {"leave_types": leave_types}} + + return HttpResponse( + encrypt_response(response, aes_key, iv), content_type="text/plain" + ) + except Exception as e: + print(e) + return JsonResponse({}, status=500) + + +def get_leave_types(): + """ + Fetch leave types from database or settings. + You can modify this function based on your data source. + """ + # Option 1: If you have a LeaveType model + try: + from leave.models import LeaveType + + leave_types = list(LeaveType.objects.values("id", "name")) + # Convert any non-string IDs to strings to match the schema + for leave_type in leave_types: + leave_type["id"] = str(leave_type["id"]) + leave_type["title"] = str(leave_type["name"]) + del leave_type["name"] + + return leave_types + + except ImportError: + # Option 2: Return hardcoded values if no model exists + return [ + {"id": "1", "title": "Casual Leave"}, + {"id": "2", "title": "Sick Leave"}, + {"id": "3", "title": "Paid Leave"}, + ] + + +def decrypt_request(encrypted_flow_data_b64, encrypted_aes_key_b64, initial_vector_b64): + flow_data = b64decode(encrypted_flow_data_b64) + iv = b64decode(initial_vector_b64) + + # Decrypt the AES encryption key + encrypted_aes_key = b64decode(encrypted_aes_key_b64) + private_key = load_pem_private_key(PRIVATE_KEY.encode("utf-8"), password=None) + aes_key = private_key.decrypt( + encrypted_aes_key, + OAEP( + mgf=MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None + ), + ) + + # Decrypt the Flow data + encrypted_flow_data_body = flow_data[:-16] + encrypted_flow_data_tag = flow_data[-16:] + decryptor = Cipher( + algorithms.AES(aes_key), modes.GCM(iv, encrypted_flow_data_tag) + ).decryptor() + decrypted_data_bytes = ( + decryptor.update(encrypted_flow_data_body) + decryptor.finalize() + ) + decrypted_data = json.loads(decrypted_data_bytes.decode("utf-8")) + return decrypted_data, aes_key, iv + + +def encrypt_response(response, aes_key, iv): + # Flip the initialization vector + flipped_iv = bytearray() + for byte in iv: + flipped_iv.append(byte ^ 0xFF) + + # Encrypt the response data + encryptor = Cipher(algorithms.AES(aes_key), modes.GCM(flipped_iv)).encryptor() + return b64encode( + encryptor.update(json.dumps(response).encode("utf-8")) + + encryptor.finalize() + + encryptor.tag + ).decode("utf-8") + + +def is_request_signature_valid(request, app_secret, request_body): + if not app_secret: + print( + "App Secret is not set up. Please Add your app secret in settings to check for request validation" + ) + return True + + signature_header = request.META.get("HTTP_X_HUB_SIGNATURE_256", "") + if not signature_header.startswith("sha256="): + print("Invalid signature header") + return False + + signature = signature_header[7:] # Remove "sha256=" + + hmac_digest = hmac.new( + app_secret.encode(), request_body.encode(), hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(signature, hmac_digest) + +# ---------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/whatsapp/filters.py b/whatsapp/filters.py new file mode 100644 index 000000000..38d249f10 --- /dev/null +++ b/whatsapp/filters.py @@ -0,0 +1,11 @@ +import django_filters +from horilla.filters import HorillaFilterSet +from whatsapp.models import WhatsappCredientials + + +class CredentialsViewFilter(HorillaFilterSet): + search = django_filters.CharFilter(field_name="meta_phone_number", lookup_expr="icontains") + + class Meta: + model = WhatsappCredientials + fields = ["meta_phone_number", "search"] diff --git a/whatsapp/flows.py b/whatsapp/flows.py new file mode 100644 index 000000000..b440cf1fe --- /dev/null +++ b/whatsapp/flows.py @@ -0,0 +1,883 @@ +import json +from asset.models import AssetCategory +from base.models import Department, EmployeeShift, JobPosition, Tags, WorkType +from employee.models import Employee +from helpdesk.models import TicketType +from leave.models import LeaveType + + +def get_asset_category_flow_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Asset Request", + "terminal": True, + "data": { + "asset_categories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "Laptops"}, + {"id": "2", "title": "Bags"}, + ], + } + }, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "Dropdown", + "label": "Asset Category", + "required": True, + "name": "asset_category", + "data-source": "${data.asset_categories}", + }, + { + "type": "TextArea", + "label": "Description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "asset_category": "${form.asset_category}", + "description": "${form.description}", + "type": "asset_request", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_attendance_request_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Attendance Request 1 of 2", + "data": { + "shift": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "Regular Shift"}, + {"id": "2", "title": "Morning Shift"}, + {"id": "3", "title": "Night Shift"}, + ], + }, + "work_type": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "WFH"}, + {"id": "2", "title": "WFO"}, + ], + } + }, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "DatePicker", + "label": "Attendance Date", + "required": True, + "name": "attendance_date", + }, + { + "type": "Dropdown", + "label": "Shift", + "required": True, + "name": "shift", + "data-source": "${data.shift}", + }, + { + "type": "Dropdown", + "label": "Work Type", + "required": True, + "name": "work_type", + "data-source": "${data.work_type}", + }, + { + "type": "TextArea", + "label": "Request Description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Continue", + "on-click-action": { + "name": "navigate", + "next": { + "type": "screen", + "name": "screen_two", + }, + "payload": { + "attendance_date": "${form.attendance_date}", + "shift": "${form.shift}", + "work_type": "${form.work_type}", + "description": "${form.description}", + }, + }, + }, + ], + } + ], + }, + }, + { + "id": "screen_two", + "title": "Attendance Request 2 of 2", + "data": { + "attendance_date": {"type": "string", "__example__": "Example"}, + "shift": {"type": "string", "__example__": "Example"}, + "work_type": {"type": "string", "__example__": "Example"}, + "description": {"type": "string", "__example__": "Example"}, + }, + "terminal": True, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "DatePicker", + "label": "Check In Date", + "required": True, + "name": "check_in_date", + }, + { + "type": "TextInput", + "label": "Check In Time", + "name": "check_in_time", + "required": True, + "input-type": "text", + "helper-text": "Check in time in HH:MM:SS (24 HRS format)", + }, + { + "type": "DatePicker", + "label": "Check Out Date", + "required": True, + "name": "check_out_date", + }, + { + "type": "TextInput", + "label": "Check Out Time", + "name": "check_out_time", + "required": True, + "input-type": "text", + "helper-text": "Check out time in HH:MM:SS (24 HRS format)", + }, + { + "type": "TextInput", + "label": "Worked Hours", + "name": "worked_hours", + "required": True, + "input-type": "text", + "helper-text": "Worked hours in HH:MM", + }, + { + "type": "TextInput", + "label": "Minimum Hours", + "name": "minimum_hours", + "required": True, + "input-type": "text", + "helper-text": "Minimum hours in HH:MM", + }, + { + "type": "Footer", + "label": "Continue", + "on-click-action": { + "name": "complete", + "payload": { + "check_in_date": "${form.check_in_date}", + "check_in_time": "${form.check_in_time}", + "check_out_date": "${form.check_out_date}", + "check_out_time": "${form.check_out_time}", + "worked_hours": "${form.worked_hours}", + "minimum_hours": "${form.minimum_hours}", + "attendance_date": "${data.attendance_date}", + "shift": "${data.shift}", + "work_type": "${data.work_type}", + "description": "${data.description}", + "type": "attendance_request", + }, + }, + }, + ], + } + ], + }, + }, + ], + } + + return flow_json + + +def get_shift_request_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Shift Request", + "terminal": True, + "data": { + "shift": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "Regular Shift"}, + {"id": "2", "title": "Morning Shift"}, + {"id": "3", "title": "Night Shift"}, + ], + } + }, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "Dropdown", + "label": "Shift", + "name": "shift", + "required": True, + "data-source": "${data.shift}", + }, + { + "type": "DatePicker", + "label": "Requested Date", + "name": "requested_date", + "required": True, + }, + { + "type": "DatePicker", + "label": "Requested Till", + "name": "requested_till", + "required": True, + }, + { + "type": "TextArea", + "label": "Description", + "name": "description", + "required": True, + }, + { + "type": "OptIn", + "label": "permentent request", + "name": "permenent_request", + "required": False, + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "shift": "${form.shift}", + "requested_date": "${form.requested_date}", + "requested_till": "${form.requested_till}", + "description": "${form.description}", + "permanent": "${form.permenent_request}", + "type": "shift_request", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_work_type_request_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Work Type Request", + "terminal": True, + "data": { + "work_type": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "WFO"}, + {"id": "2", "title": "WFH"}, + ], + } + }, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "Dropdown", + "label": "work type", + "required": True, + "name": "work_type", + "data-source": "${data.work_type}", + }, + { + "type": "DatePicker", + "label": "Requested date", + "required": True, + "name": "request_date", + }, + { + "type": "DatePicker", + "label": "requested till", + "required": True, + "name": "requested_till", + }, + { + "type": "TextArea", + "label": "description", + "required": True, + "name": "description", + }, + { + "type": "OptIn", + "label": "permentent request", + "required": False, + "name": "permenent_request", + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "work_type": "${form.work_type}", + "requested_date": "${form.request_date}", + "requested_till": "${form.requested_till}", + "description": "${form.description}", + "permenent_request": "${form.permenent_request}", + "type": "work_type", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_leave_request_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Leave request", + "terminal": True, + "data": { + "leave_types": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "title": {"type": "string"}, + }, + "required": ["id", "title"], + }, + "__example__": [ + {"id": "1", "title": "Casual Leave"}, + {"id": "2", "title": "Sick Leave"}, + {"id": "3", "title": "Paid Leave"}, + ], + } + }, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "Dropdown", + "label": "Leave type", + "required": True, + "name": "leave_type", + "data-source": "${data.leave_types}", + }, + { + "type": "DatePicker", + "label": "Start Date", + "required": True, + "name": "start_date", + }, + { + "type": "Dropdown", + "label": "Start Date breakdown", + "required": True, + "name": "start_date_breakdown", + "data-source": [ + {"id": "full_day", "title": "Full day"}, + {"id": "first_half", "title": "First half"}, + {"id": "second_half", "title": "Second Half"}, + ], + }, + { + "type": "DatePicker", + "label": "End Date", + "required": True, + "name": "end_date", + }, + { + "type": "Dropdown", + "label": "End Date Breakdown", + "required": True, + "name": "end_date_breakdown", + "data-source": [ + {"id": "full_day", "title": "Full Day"}, + {"id": "first_half", "title": "First Half"}, + {"id": "second_half", "title": "Second Half"}, + ], + }, + { + "type": "DocumentPicker", + "name": "document_picker", + "label": "Upload photos", + "description": "Please attach images about the received items", + "max-file-size-kb": 10240, + "max-uploaded-documents": 1, + }, + { + "type": "TextArea", + "label": "Description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "leave_type": "${form.leave_type}", + "start_date": "${form.start_date}", + "start_date_breakdown": "${form.start_date_breakdown}", + "end_date": "${form.end_date}", + "end_date_breakdown": "${form.end_date_breakdown}", + "description": "${form.description}", + "document_picker": "${form.document_picker}", + "type": "leave_request", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_reimbursement_request_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Reimbursements", + "data": {}, + "terminal": True, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "TextInput", + "label": "Title", + "name": "title", + "required": True, + "input-type": "text", + }, + { + "type": "DatePicker", + "label": "Allowance on", + "required": True, + "name": "allowance_date", + }, + { + "type": "TextInput", + "label": "Amount", + "name": "amount", + "required": True, + "input-type": "number", + }, + { + "type": "DocumentPicker", + "name": "document_picker", + "label": "Upload photos", + "description": "Please attach images about the received items", + "max-file-size-kb": 10240, + "max-uploaded-documents": 10, + }, + { + "type": "TextArea", + "label": "description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "title": "${form.title}", + "allowance_date": "${form.allowance_date}", + "document_picker": "${form.document_picker}", + "amount": "${form.amount}", + "description": "${form.description}", + "type": "reimbursement", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_bonus_point_json(): + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Bonus Point", + "data": {}, + "terminal": True, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "TextInput", + "label": "Title", + "name": "title", + "required": True, + "input-type": "text", + }, + { + "type": "DatePicker", + "label": "Allowance on", + "required": True, + "name": "allowance_on", + }, + { + "type": "TextInput", + "label": "Bonus Points", + "name": "bonus_point", + "required": True, + "input-type": "number", + }, + { + "type": "TextArea", + "label": "description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "title": "${form.title}", + "allowance_on": "${form.allowance_on}", + "bonus_point": "${form.bonus_point}", + "description": "${form.description}", + "type": "bonus_point", + }, + }, + }, + ], + } + ], + }, + } + ], + } + + return flow_json + + +def get_ticket_json(): + + ticket_type_data = [ + {"id": str(ticket_type.id), "title": ticket_type.title} + for ticket_type in TicketType.objects.all() + ] + + priority_data = [ + {"id": "low", "title": "Low"}, + {"id": "medium", "title": "Medium"}, + {"id": "high", "title": "High"}, + ] + + assigning_type_data = [ + {"id": "department", "title": "Department"}, + {"id": "job_position", "title": "Job Position"}, + {"id": "individual", "title": "Individual"}, + ] + + department_data = [ + {"id": str(department.id), "title": department.department} + for department in Department.objects.all() + ] + + job_position_data = [ + {"id": str(job_position.id), "title": job_position.job_position} + for job_position in JobPosition.objects.all() + ] + + individual_data = [ + {"id": str(individual.id), "title": individual.get_full_name()} + for individual in Employee.objects.all() + ] + + tags_data = [{"id": str(tag.id), "title": tag.title} for tag in Tags.objects.all()] + + flow_json = { + "version": "5.0", + "screens": [ + { + "id": "screen_one", + "title": "Raise Ticket 1 of 2", + "data": {}, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "TextInput", + "name": "title", + "label": "Title", + "required": True, + "input-type": "text", + }, + { + "type": "Dropdown", + "label": "Ticket Type", + "required": True, + "name": "ticket_type", + "data-source": ticket_type_data, + }, + { + "type": "Dropdown", + "label": "Priority", + "required": True, + "name": "priority", + "data-source": priority_data, + }, + { + "type": "TextArea", + "label": "description", + "required": True, + "name": "description", + }, + { + "type": "Footer", + "label": "Continue", + "on-click-action": { + "name": "navigate", + "next": { + "type": "screen", + "name": "screen_quhode", + }, + "payload": { + "title": "${form.title}", + "ticket_type": "${form.ticket_type}", + "priority": "${form.priority}", + "description": "${form.description}", + }, + }, + }, + ], + } + ], + }, + }, + { + "id": "screen_two", + "title": "Raise Ticket 2 of 2", + "data": { + "title": { + "type": "string", + "__example__": "Example", + }, + "ticket_type": {"type": "string", "__example__": "Example"}, + "priority": {"type": "string", "__example__": "Example"}, + "description": {"type": "string", "__example__": "Example"}, + }, + "terminal": True, + "layout": { + "type": "SingleColumnLayout", + "children": [ + { + "type": "Form", + "name": "flow_path", + "children": [ + { + "type": "Dropdown", + "label": "Assigning Type", + "required": True, + "name": "assigning_type", + "data-source": assigning_type_data, + }, + { + "type": "Dropdown", + "label": "Forward To", + "required": True, + "name": "forward_to", + "data-source": [ + {"id": "0_Option_1", "title": "Option 1"}, + {"id": "1_Option_2", "title": "Option 2"}, + ], + }, + { + "type": "DatePicker", + "label": "Deadline", + "required": True, + "name": "deadline", + }, + { + "type": "Dropdown", + "label": "Tags", + "required": False, + "name": "tags", + "data-source": tags_data, + }, + { + "type": "Footer", + "label": "Save", + "on-click-action": { + "name": "complete", + "payload": { + "screen_1_Dropdown_0": "${form.assigning_type}", + "screen_1_Dropdown_1": "${form.forward_to}", + "screen_1_DatePicker_2": "${form.deadline}", + "screen_1_Dropdown_3": "${form.tags}", + "title": "${data.title}", + "ticket_type": "${data.ticket_type}", + "priority": "${data.priority}", + "description": "${data.description}", + "type": "ticket", + }, + }, + }, + ], + } + ], + }, + }, + ], + } + return json.dumps(flow_json) diff --git a/whatsapp/forms.py b/whatsapp/forms.py new file mode 100644 index 000000000..01f1fe65f --- /dev/null +++ b/whatsapp/forms.py @@ -0,0 +1,45 @@ +# forms.py +from typing import Any +from django.forms import ValidationError +from django.template.loader import render_to_string + +from base.forms import ModelForm +from whatsapp.models import WhatsappCredientials + + +class WhatsappForm(ModelForm): + cols = {"meta_token":12} + class Meta: + model = WhatsappCredientials + fields = '__all__' + exclude = ["is_active", "created_templates"] + + + 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() + companies = cleaned_data.get("company_id") + is_primary = cleaned_data.get("is_primary") + + if companies: + for company in companies: + existing_primary = WhatsappCredientials.objects.filter( + company_id=company, is_primary=True + ).exclude(id=self.instance.id) + + if is_primary: + if existing_primary.exists(): + raise ValidationError(f"Company '{company.company}' already has a primary credential.") + else: + if not existing_primary.exists(): + cleaned_data["is_primary"] = True + + return cleaned_data diff --git a/whatsapp/migrations/__init__.py b/whatsapp/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/whatsapp/models.py b/whatsapp/models.py new file mode 100644 index 000000000..74630694e --- /dev/null +++ b/whatsapp/models.py @@ -0,0 +1,113 @@ +from django.db import models +from django.urls import reverse + +from base.horilla_company_manager import HorillaCompanyManager +from base.models import Company +from horilla.models import HorillaModel +from horilla_views.cbv_methods import render_template + + +class WhatsappCredientials(HorillaModel): + meta_token = models.CharField(max_length=255) + meta_business_id = models.CharField(max_length=255) + meta_phone_number_id = models.CharField(max_length=255) + meta_phone_number = models.CharField(max_length=20) + created_templates = models.BooleanField(default=False) + meta_webhook_token = models.CharField( + max_length=50, + verbose_name="Webhook Token", + help_text="This token is used to connect webhook to the server", + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name="Company") + is_primary = models.BooleanField(default=False) + + # objects = HorillaCompanyManager() + + def __str__(self): + return f"WhatsApp Business {self.meta_business_id} ({self.meta_phone_number})" + + def token_render(self): + + alert = """ + Swal.fire({ + text: "Token copied", + icon: "success", + showConfirmButton: false, + timer: 2000, + timerProgressBar: true, + }); + """ + + html = f""" + {self.meta_token[:20]}... + + """ + return html + + def get_update_url(self): + url = reverse("whatsapp-credential-update", kwargs={"pk": self.pk}) + return url + + def get_publish_button(self): + html = render_template( + path="whatsapp/option_buttons.html", context={"instance": self} + ) + return html + + def get_primary(self): + if self.is_primary: + return "style='background:#ffa60028'" + + def get_instance(self): + """ + used to return the id of the instance + Returns: + id of the instance + """ + return self.pk + + def get_delete_url(self): + url = reverse("whatsapp-credential-delete") + id = self.pk + url = f"{url}?id={id}" + return url + + def get_test_message_url(self): + url = reverse("send-test-message") + return url + + def get_webhook_token(self): + placeholder = "•" * len(self.meta_webhook_token) + html = f""" + + + """ + return html + + +class WhatsappFlowDetails(models.Model): + template = models.CharField(max_length=50) + flow_id = models.CharField(max_length=50) + whatsapp_id = models.ForeignKey(WhatsappCredientials, on_delete=models.CASCADE) + + def __str__(self) -> str: + return self.template diff --git a/whatsapp/templates/whatsapp/credentials_view.html b/whatsapp/templates/whatsapp/credentials_view.html new file mode 100644 index 000000000..d65689a8a --- /dev/null +++ b/whatsapp/templates/whatsapp/credentials_view.html @@ -0,0 +1,53 @@ +{% extends 'settings.html' %} +{% load i18n static %} +{% block settings %} + +{% for path in style_path %} + +{% endfor %} +{% for path in script_static_paths %} + +{% endfor %} + +{% include "generic/components.html" %} + + + +
    + {% if perms.base.view_department %} + {% if perms.base.add_department %} +
    +
    + {% endif %} +
    + +
    +
    +
    +
    + {% endif %} + + + + + + + +{% endblock settings %} \ No newline at end of file diff --git a/whatsapp/templates/whatsapp/option_buttons.html b/whatsapp/templates/whatsapp/option_buttons.html new file mode 100644 index 000000000..af3da5efd --- /dev/null +++ b/whatsapp/templates/whatsapp/option_buttons.html @@ -0,0 +1,32 @@ +{% load static i18n %} {% if not instance.created_templates %} + + + +{% else %} + + + +{% endif %} diff --git a/whatsapp/templates/whatsapp/send_test_message_form.html b/whatsapp/templates/whatsapp/send_test_message_form.html new file mode 100644 index 000000000..a01bb1f5a --- /dev/null +++ b/whatsapp/templates/whatsapp/send_test_message_form.html @@ -0,0 +1,27 @@ +{% load i18n %} + +
    +

    + {% trans "Send Test Message" %} +

    + +
    +
    + {% csrf_token %} +
    +
    +
    + + +
    +
    +
    + +
    + \ No newline at end of file diff --git a/whatsapp/templates/whatsapp/whatsapp_animation.html b/whatsapp/templates/whatsapp/whatsapp_animation.html new file mode 100644 index 000000000..6e63d72d1 --- /dev/null +++ b/whatsapp/templates/whatsapp/whatsapp_animation.html @@ -0,0 +1,91 @@ +{% load i18n %} {% load static %} + +
    + + + + + + + + + + + + + + + + + + + + {% trans "Trying to connect..." %} +
    diff --git a/whatsapp/tests.py b/whatsapp/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/whatsapp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/whatsapp/urls.py b/whatsapp/urls.py new file mode 100644 index 000000000..dec63ecc4 --- /dev/null +++ b/whatsapp/urls.py @@ -0,0 +1,63 @@ +from django.urls import path +from . import views +from whatsapp.cbv import whatsapp + +urlpatterns = [ + path("", views.whatsapp, name="whatsapp"), + path( + "template-creation/", + views.create_flows, + name="template-creation", + ), + path( + "generic-template-creation//", + views.create_generic_templates, + name="generic-template-creation", + ), + # path( + # "end-point/", + # views.end_point, + # name="end-point", + # ), + # path( + # "leave-request/", + # views.end_point, + # name="leave-request", + # ), + + path( + "whatsapp-credential-view/", + views.whatsapp_credential_view, + name="whatsapp-credential-view", + ), + path( + "whatsapp-credential-list/", + whatsapp.CredentialListView.as_view(), + name="whatsapp-credential-list", + ), + path( + "whatsapp-credential-nav/", + whatsapp.CredentialNav.as_view(), + name="whatsapp-credential-nav", + ), + path( + "whatsapp-credential-create/", + whatsapp.CredentialForm.as_view(), + name="whatsapp-credential-create", + ), + path( + "whatsapp-credential-update//", + whatsapp.CredentialForm.as_view(), + name="whatsapp-credential-update", + ), + path( + "whatsapp-credential-delete", + whatsapp.delete_credentials, + name="whatsapp-credential-delete", + ), + path( + "send-test-message", + whatsapp.send_test_message, + name="send-test-message", + ), +] diff --git a/whatsapp/utils.py b/whatsapp/utils.py new file mode 100644 index 000000000..70eacd599 --- /dev/null +++ b/whatsapp/utils.py @@ -0,0 +1,971 @@ +import json +import requests +import inspect + +from django.http import QueryDict +from django.test import RequestFactory +from django.middleware.csrf import get_token +from django.utils.datastructures import MultiValueDict +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile +from django.contrib.sessions.backends.db import SessionStore +from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.messages.storage.fallback import FallbackStorage + +from asset.models import AssetCategory +from base.models import Company, EmployeeShift, WorkType +from employee.models import Employee +from horilla.horilla_middlewares import _thread_locals +from whatsapp.models import WhatsappCredientials, WhatsappFlowDetails + + +# PERM_TOKEN = "EAAM3cI4xxBkBO6fvkk6TjpkZC0TKLeFk4YBGUp6ZBJZCmNhcjcrmcX0VMrUnvlYgnmErFWMNlZAvRfnZAboFDl4eTuuuO3a4LH8ZB5CWFuiF9GDXdHw1NYB9UCHKMBGIVsVH1GNb3JVqmcrokfq7iABRtZBPEZA3pyDPWXmkN06gu1RyfjV6hQe6cl9wvO1AgmkhLgZDZD" +# META_TOKEN = "EAAM3cI4xxBkBOwevhATEliQ7GI4S2WMZCdmX lJ5wiZCu1o3xSvQUZCAlVL7scfbUXlZBkIHEbaFJGw094vR4v7CmgBtXNqy68InXJZCg9sL2ZB4ZCgORUNZCWd7o92cNzZBQ07pgj8vF0ZB4KRNQMoUVlFZAqLGA5EOLEgsXjZAbZAndiqKRUBeZA3ytpICIVuVuWPGuRTGa8lDLAgZBIwCRqhnM5oZD" +# META_TOKEN = PERM_TOKEN + +class CustomRequestFactory(RequestFactory): + """ + Custom request factory to create mock POST requests with session and messages enabled. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.FILES = MultiValueDict() + + def _add_session_and_messages(self, request): + """ + Add session and messages middleware to the request. + + Args: + request: Django HttpRequest object to add session and messages. + + Returns: + Modified request with session and messages added. + """ + + middleware = SessionMiddleware(lambda req: None) + middleware.process_request(request) + request.session.save() + request._messages = FallbackStorage(request) + + +def get_meta_details_from_number(number): + emp_company = Employee.objects.filter(phone=number).first().get_company() + company = emp_company if emp_company else Company.objects.filter(hq=True).first() + + credentials = WhatsappCredientials.objects.filter(company_id=company) + credentials = ( + credentials.get(is_primary=True) + if credentials.get(is_primary=True) + else credentials.first() + ) + url = ( + f"https://graph.facebook.com/v21.0/{credentials.meta_phone_number_id}/messages" + ) + data = { + "token": credentials.meta_token, + "url": url, + "business_id": credentials.meta_business_id, + "credentials": credentials, + } + + return data + + +def get_meta_details_from_id(cred_id): + credentials = WhatsappCredientials.objects.get(id=cred_id) + url = ( + f"https://graph.facebook.com/v21.0/{credentials.meta_phone_number_id}/messages" + ) + data = { + "token": credentials.meta_token, + "url": url, + "business_id": credentials.meta_business_id, + } + return data + + +def create_template_buttons(cred_id): + """ + Creates a message template with buttons for different request types. + + Sends a POST request to the WhatsApp Business API to create a new template + that includes various quick reply buttons for the user to choose from. + + Returns: + dict: The JSON response from the API containing the status of the request. + """ + data = get_meta_details_from_id(cred_id) + api_url = f'https://graph.facebook.com/v21.0/{data.get("business_id","")}/message_templates' + token = data.get("token", "") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + buttons = [ + "Asset Request", + "Attendance Request", + "Bonus Point Redeem", + "Leave Request", + "Reimbursement Request", + "Shift Request", + "Work Type Request", + ] + + quick_reply_buttons = [{"type": "QUICK_REPLY", "text": btn} for btn in buttons] + + payload = { + "name": "button_template", + "language": "en_US", + "category": "utility", + "components": [ + {"type": "HEADER", "format": "TEXT", "text": "Create Request"}, + { + "type": "BODY", + "text": "Choose a button from below to create the requests", + }, + {"type": "BUTTONS", "buttons": quick_reply_buttons}, + ], + } + + try: + response = requests.post(api_url, headers=headers, json=payload) + print(response.json()) + if response.status_code == 200: + return response.json() + else: + return {"status": response.json()} + except requests.exceptions.RequestException as e: + return {"status": "error", "message": str(e)} + + +def create_welcome_message(cred_id): + """ + Creates a welcome message template. + + Sends a POST request to the WhatsApp Business API to create a welcome message + template that responds to users when they contact the business. + + Returns: + dict: The JSON response from the API containing the status of the request. + """ + data = get_meta_details_from_id(cred_id) + api_url = f'https://graph.facebook.com/v21.0/{data.get("business_id","")}/message_templates' + token = data.get("token", "") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + payload = { + "name": "welcome_message", + "language": "en_US", + "category": "utility", + "components": [ + { + "type": "BODY", + "text": "Thanks for conatcting us, For further help send *'help'*. Thank you", + }, + ], + } + + try: + response = requests.post(api_url, headers=headers, json=payload) + print(response.json()) + if response.status_code == 200: + return response.json() + else: + return {"status": response.json()} + except requests.exceptions.RequestException as e: + return {"status": "error", "message": str(e)} + + +def create_help_message(cred_id): + """ + Creates a help message template. + + Sends a POST request to the WhatsApp Business API to create a help message template + that provides users with instructions on how to create various forms. + + Returns: + dict: The JSON response from the API containing the status of the request. + """ + data = get_meta_details_from_id(cred_id) + api_url = f'https://graph.facebook.com/v21.0/{data.get("business_id","")}/message_templates' + token = data.get("token", "") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + body_text = """ + To create a request, send any of the following keywords. You can use capital or small letters, and spaces can be included or excluded. + + Asset Request : Send *'asset'* + Bonus Point Redeem : Send *'bonus'* or *'bonus point'* + Attendance Request : Send *'attendance'* + Leave Request : Send *'leave'* + Reimbursement Request : Send *'reimbursement'* + Shift Request : Send *'shift'* + Work Type Request : Send *'work type'* + """ + + payload = { + "name": "help_text", + "language": "en_US", + "category": "utility", + "components": [ + {"type": "HEADER", "format": "TEXT", "text": "Keywords"}, + {"type": "BODY", "text": body_text}, + ], + } + + try: + response = requests.post(api_url, headers=headers, json=payload) + print(response.json()) + if response.status_code == 200: + return response.json() + else: + return {"status": response.json()} + except requests.exceptions.RequestException as e: + return {"status": "error", "message": str(e)} + + +def send_image_message(number, link): + """ + Sends an image message to a specific WhatsApp number. + + Args: + number (str): The recipient's phone number. + link (str): The URL link to the image to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + data = get_meta_details_from_number(number) + headers = { + "Authorization": f'Bearer {data.get("token","")}', + "Content-Type": "application/json", + } + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": number, + "type": "image", + "image": { + "link": link, + }, + } + response = requests.post(data.get("url", ""), json=payload, headers=headers) + + print(response.json()) + return response.status_code == 200 + + +def send_document_message(number, link): + """ + Sends an image message to a specific WhatsApp number. + + Args: + number (str): The recipient's phone number. + link (str): The URL link to the image to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + + data = get_meta_details_from_number(number) + headers = { + "Authorization": f'Bearer {data.get("token","")}', + "Content-Type": "application/json", + } + url = data.get("url", "") + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": number, + "type": "document", + "document": { + "link": link, + }, + } + response = requests.post(url, json=payload, headers=headers) + + print(response.json()) + return response.status_code == 200 + + +def send_text_message(number, message, header=None): + """ + Sends a text message to a specific WhatsApp number. + + Args: + number (str): The recipient's phone number. + message (str): The text message to be sent. + + Returns: + bool: True if the message was sent successfully, False otherwise. + """ + data = get_meta_details_from_number(number) + headers = { + "Authorization": f'Bearer {data.get("token","")}', + "Content-Type": "application/json", + } + url = data.get("url", "") + + full_message = f"*{header}*\n\n{message}" if header else message + + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": number, + "type": "text", + "text": { + "body": full_message, + }, + } + + response = requests.post(url, json=payload, headers=headers) + print(response.json()) + return response.status_code == 200 + + +def send_template_message(number, template_name, ln_code="en_US"): + """ + Sends a template message to a specific WhatsApp number. + + Args: + number (str): The recipient's phone number. + template_name (str): The name of the template to be sent. + ln_code (str): The language code for the message (default is "en_US"). + + Returns: + dict: The JSON response from the API containing the status of the request. + """ + + data = get_meta_details_from_number(number) + headers = { + "Authorization": f'Bearer {data.get("token","")}', + "Content-Type": "application/json", + } + url = data.get("url", "") + + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": number, + "type": "template", + "template": { + "name": template_name, + "language": {"code": ln_code}, + "components": [], + }, + } + response = requests.post(url, headers=headers, json=payload) + print(response.json()) + return response + + +def send_flow_message(to, template_name): + """ + Sends an interactive flow message to a specific WhatsApp number. + + Args: + to (str): The recipient's phone number. + template_name (str): The name of the flow template to be sent. + + Returns: + dict: The JSON response from the API containing the status of the request. + """ + + data = get_meta_details_from_number(to) + headers = { + "Authorization": f'Bearer {data.get("token","")}', + "Content-Type": "application/json", + } + url = data.get("url", "") + + flow_id = ( + WhatsappFlowDetails.objects.filter( + template=template_name, whatsapp_id=data.get("credentials").id + ) + .first() + .flow_id + ) + employee = Employee.objects.get(phone=to) + details = flow_message_details(template_name, employee) + + flow_paylod = { + "screen": "screen_one", + } + if details.get("data"): + flow_paylod["data"] = details.get("data") + + payload = { + "recipient_type": "individual", + "messaging_product": "whatsapp", + "to": to, + "type": "interactive", + "interactive": { + "type": "flow", + "header": {"type": "text", "text": details.get("header", None)}, + "body": {"text": details.get("body", None)}, + "action": { + "name": "flow", + "parameters": { + "flow_message_version": "3", + "flow_token": "flow_token", + "flow_id": flow_id, + "flow_cta": details.get("button", {}), + "flow_action_payload": flow_paylod, + }, + }, + }, + } + + response = requests.post(url, headers=headers, json=payload) + + print(response.json()) + return response.json() + + +def flow_message_details(template_name, employee): + """ + Retrieves the flow message details for a specific template name. + + Args: + template_name (str): The name of the flow template. + employee (Employee): The employee object containing available leave and shift data. + + Returns: + dict: A dictionary containing details like header, body, button, and associated data for the flow. + """ + + leave_types_data = [ + { + "id": str(available_leave.leave_type_id.id), + "title": available_leave.leave_type_id.name, + } + for available_leave in employee.available_leave.all() + ] + shift_data = [ + {"id": str(shift.id), "title": shift.employee_shift} + for shift in EmployeeShift.objects.all() + ] + work_type_data = [ + {"id": str(work_type.id), "title": work_type.work_type} + for work_type in WorkType.objects.all() + ] + asset_data = [ + {"id": str(asset.id), "title": asset.asset_category_name} + for asset in AssetCategory.objects.all() + ] + + details = { + "leave": { + "header": "Leave Request", + "body": "To proceed with your leave request, Please fill out the following form", + "button": "Add Leave Request", + "data": {"leave_types": leave_types_data}, + }, + "asset": { + "header": "Asset Request", + "body": "To proceed with your asset request, Please fill out the following form", + "button": "Add Asset Request", + "data": {"asset_categories": asset_data}, + }, + "shift": { + "header": "Shift Request", + "body": "To proceed with your shift request, Please fill out the following form", + "button": "Add Shift Request", + "data": {"shift": shift_data}, + }, + "work_type": { + "header": "Work Type Request", + "body": "To proceed with your work type request, Please fill out the following form", + "button": "Add Work Type Request", + "data": {"work_type": work_type_data}, + }, + "attendance": { + "header": "Attendnace Request", + "body": "To proceed with your attendnace request, Please fill out the following form", + "button": "Add Attendnace Request", + "data": {"shift": shift_data, "work_type": work_type_data}, + }, + "bonus_point": { + "header": "Bonus Point Redeem Request", + "body": "To proceed with your bonus point redeem request, Please fill out the following form", + "button": "Redeem Bonus point", + }, + "reimbursement": { + "header": "Reimbursement Request", + "body": "To proceed with your reimbursement request, Please fill out the following form", + "button": "Add Reimbursement Request", + }, + } + + return details.get(template_name) + + +def create_request( + employee, + flow_response, + data, + form_class, + view_function, + message_header, + attachments=None, +): + """ + Generic function to create a request, populate form data, validate, and call a view. + + Args: + employee: Employee instance, used for user and employee-specific data. + flow_response: Dictionary containing specific data for the request. + data: Form data dictionary specific to the request type. + form_class: Form class to validate request data. + view_function: View function to process the request. + attachments: List of files (optional) to include in the request. + + Returns: + Success or error message from form validation and view execution. + """ + + factory = CustomRequestFactory() + request = factory.post("/") + request.session = SessionStore() + request.session.create() + token = get_token(request) + + data["csrfmiddlewaretoken"] = token + request.POST = QueryDict("", mutable=True) + request.POST.update(data) + request.user = employee.employee_user_id + factory._add_session_and_messages(request) + _thread_locals.request = request + + if attachments: + for idx, attachment in enumerate(attachments): + request.FILES.appendlist( + "attachment", + SimpleUploadedFile( + attachment.name, + attachment.read(), + content_type="application/octet-stream", + ), + ) + + request.POST = data + form = form_class(request.POST, files=request.FILES if attachments else None) + if form.is_valid(): + message = f"{message_header} created successfully" + else: + form_errors = dict(form.errors) + message = "\n".join( + f"{message_header if field == '__all__' else field}: {', '.join(errors)}" + for field, errors in form_errors.items() + ) + try: + function = inspect.unwrap(view_function) + _response = function(request=request) + except Exception as e: + print(f"Error in {view_function.__name__}: {e}") + + return message + + +def shift_create(employee, flow_response): + """ + Creates a shift request for the specified employee using the provided flow response data. + + Args: + employee: Employee instance submitting the shift request. + flow_response: Dictionary containing shift-specific data like shift ID and dates. + + Returns: + Message indicating the success or failure of the request. + """ + + from base.forms import ShiftRequestForm + from base.views import shift_request as shift_request_creation + + data = { + "employee_id": employee.id, + "shift_id": flow_response["shift"], + "requested_date": flow_response["requested_date"], + "requested_till": flow_response["requested_till"], + "is_permanent_shift": flow_response.get("permenent_request", False), + "description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + ShiftRequestForm, + shift_request_creation, + "Shift request", + ) + + +def work_type_create(employee, flow_response): + """ + Creates a work type request for the specified employee. + + Args: + employee: Employee instance submitting the work type request. + flow_response: Dictionary with work type request data. + + Returns: + Message indicating the success or failure of the request. + """ + + from base.forms import WorkTypeRequestForm + from base.views import work_type_request as work_type_request_creation + + data = { + "employee_id": employee.id, + "work_type_id": flow_response["work_type"], + "requested_date": flow_response["requested_date"], + "requested_till": flow_response["requested_till"], + "is_permanent_work_type": flow_response.get("permenent_request", False), + "description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + WorkTypeRequestForm, + work_type_request_creation, + "Work type request", + ) + + +def leave_request_create(employee, flow_response): + """ + Creates a leave request for the specified employee, optionally with an attachment. + + Args: + employee: Employee instance submitting the leave request. + flow_response: Dictionary with leave-specific data, including optional attachment info. + + Returns: + Message indicating the success or failure of the request. + """ + + from leave.forms import UserLeaveRequestCreationForm + from leave.views import leave_request_create as leave_req_creation + + media_id = ( + flow_response.get("document_picker")[0]["id"] + if flow_response.get("document_picker") + else None + ) + file_name = ( + flow_response.get("document_picker")[0]["file_name"] + if flow_response.get("document_picker") + else None + ) + attachment_file = get_whatsapp_media_file(media_id, file_name) if media_id else None + + data = { + "employee_id": employee.id, + "leave_type_id": flow_response["leave_type"], + "start_date": flow_response["start_date"], + "start_date_breakdown": flow_response["start_date_breakdown"], + "end_date": flow_response["end_date"], + "end_date_breakdown": flow_response["end_date_breakdown"], + "description": flow_response["description"], + } + attachments = [attachment_file] if attachment_file else None + return create_request( + employee, + flow_response, + data, + UserLeaveRequestCreationForm, + leave_req_creation, + "Leave request", + attachments, + ) + + +def asset_request_create(employee, flow_response): + """ + Creates an asset request for the specified employee. + + Args: + employee: Employee instance submitting the asset request. + flow_response: Dictionary with asset request data. + + Returns: + Message indicating the success or failure of the request. + """ + + from asset.forms import AssetRequestForm + from asset.views import asset_request_creation + + data = { + "requested_employee_id": employee.id, + "asset_category_id": flow_response["asset_category"], + "description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + AssetRequestForm, + asset_request_creation, + "Asset request", + ) + + +def attendance_request_create(employee, flow_response): + """ + Creates an attendance request for the specified employee. + + Args: + employee: Employee instance submitting the attendance request. + flow_response: Dictionary with attendance-specific data. + + Returns: + Message indicating the success or failure of the request. + """ + + from attendance.forms import NewRequestForm + from attendance.views.requests import request_new as attendance_request_creation + + data = { + "employee_id": employee.id, + "attendance_date": flow_response["attendance_date"], + "shift_id": flow_response["shift"], + "work_type_id": flow_response["work_type"], + "attendance_clock_in_date": flow_response["check_in_date"], + "attendance_clock_in": flow_response["check_in_time"], + "attendance_clock_out_date": flow_response["check_out_date"], + "attendance_clock_out": flow_response["check_out_time"], + "attendance_worked_hour": flow_response["worked_hours"], + "minimum_hour": flow_response["minimum_hours"], + "request_description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + NewRequestForm, + attendance_request_creation, + "Attendance request", + ) + + +def bonus_point_create(employee, flow_response): + """ + Creates a bonus point redemption request for the specified employee. + + Args: + employee: Employee instance submitting the bonus point redemption request. + flow_response: Dictionary with bonus point redemption data. + + Returns: + Message indicating the success or failure of the request. + """ + + from payroll.forms.component_forms import ReimbursementForm + from payroll.views.component_views import create_reimbursement + + data = { + "employee_id": employee.id, + "title": flow_response["title"], + "type": "bonus_encashment", + "allowance_on": flow_response["allowance_on"], + "bonus_to_encash": flow_response["bonus_point"], + "description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + ReimbursementForm, + create_reimbursement, + "Bonus point redemption request", + ) + + +def reimbursement_create(employee, flow_response): + """ + Creates a reimbursement request for the specified employee, including any attachments. + + Args: + employee: Employee instance submitting the reimbursement request. + flow_response: Dictionary with reimbursement data, including optional attachments. + + Returns: + Message indicating the success or failure of the request. + """ + + from payroll.forms.component_forms import ReimbursementForm + from payroll.views.component_views import create_reimbursement + + data = get_meta_details_from_number(employee.phone) + attachments = [ + get_whatsapp_media_file(image["id"], image["file_name"], data.get("token", "")) + for image in flow_response.get("document_picker", []) + if image.get("id") and image.get("file_name") + ] + data = { + "employee_id": employee.id, + "title": flow_response["title"], + "type": "reimbursement", + "allowance_on": flow_response["allowance_date"], + "amount": flow_response["amount"], + "description": flow_response["description"], + } + return create_request( + employee, + flow_response, + data, + ReimbursementForm, + create_reimbursement, + "Reimbursemnt", + attachments, + ) + + +def get_whatsapp_media_file(media_id, file_name, token): + """ + Fetches a media file from WhatsApp using its media ID. + + Args: + media_id (str): The ID of the media to fetch. + file_name (str): The base name to use for saving the media file. + + Returns: + ContentFile: A Django ContentFile object containing the media data, or None if the fetch fails. + """ + + url = f"https://graph.facebook.com/v21.0/{media_id}" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers) + if response.status_code == 200: + media_data = response.json() + media_url = media_data.get("url") + file = download_whatsapp_media(media_url, file_name, token) + return file + else: + print(f"Failed to fetch media: {response.text}") + return None + + +def download_whatsapp_media(media_url, file_name, token): + """ + Downloads media from a given URL and saves it as a ContentFile. + + Args: + media_url (str): The URL of the media to download. + file_name (str): The name to save the downloaded media file as. + + Returns: + ContentFile: A Django ContentFile object containing the media data, or None if the download fails. + """ + + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(media_url, headers=headers) + if response.status_code == 200: + content_type = response.headers.get("Content-Type") + extension = content_type.split("/")[-1] + file_name = f"{file_name[:50]}.{extension}" + file_content = ContentFile(response.content) + file_content.name = file_name + return file_content + else: + print(f"Failed to download media: {response.status_code}") + return None + + +def create_flow(flow_name, template_name, cred_id): + """ + Creates a new flow in WhatsApp Business API. + + Args: + flow_name (str): The name of the flow to create. + template_name (str): The name of the template associated with the flow. + + Returns: + Response: The JSON response from the API, or error details if the creation fails. + """ + credential = WhatsappCredientials.objects.get(id=cred_id) + + headers = { + "Authorization": f"Bearer {credential.meta_token}", + "Content-Type": "application/json", + } + data = {"name": flow_name, "categories": "OTHER"} + response = requests.post( + f"https://graph.facebook.com/v21.0/{credential.meta_business_id}/flows", + json=data, + headers=headers, + ) + + if response.status_code != 200: + print(f"Error in creating flow: {response.json()}") + return response + + try: + flow_id = response.json().get("id") + obj, created = WhatsappFlowDetails.objects.get_or_create( + template=template_name, + whatsapp_id=credential, + defaults={"flow_id": flow_id}, + ) + if not created: + obj.flow_id = flow_id + obj.save() + return response + except KeyError: + print("Error: 'id' not found in response") + return response + + +def update_flow(flow_id, flow_json, token): + """ + Updates an existing flow with new data. + + Args: + flow_id (str): The ID of the flow to update. + flow_json (dict): The JSON object containing the new flow data. + + Returns: + Response: The JSON response from the API, or error details if the update fails. + """ + + url = f"https://graph.facebook.com/v21.0/{flow_id}/assets" + headers = {"Authorization": f"Bearer {token}"} + + with open("flow.json", "w") as file: + json.dump(flow_json, file, indent=2) + + with open("flow.json", "rb") as file: + files = { + "file": ("flow.json", file, "application/json"), + "name": (None, "flow.json"), + "asset_type": (None, "FLOW_JSON"), + } + response = requests.post(url, headers=headers, files=files) + + if response.status_code != 200: + print(f"Error in updating flow: {response.json()}") + + return response + + +def publish_flow(flow_id, token): + """ + Publishes a flow in WhatsApp Business API. + + Args: + flow_id (str): The ID of the flow to publish. + + Returns: + Response: The JSON response from the API, or error details if the publication fails. + """ + + headers = {"Authorization": f"Bearer {token}"} + response = requests.post( + f"https://graph.facebook.com/v21.0/{flow_id}/publish", headers=headers + ) + + if response.status_code != 200: + print(f"Error in publishing flow: {response.json()}") + + return response diff --git a/whatsapp/views.py b/whatsapp/views.py new file mode 100644 index 000000000..b40833bf9 --- /dev/null +++ b/whatsapp/views.py @@ -0,0 +1,491 @@ +import json +import logging +import string +import threading +from bs4 import BeautifulSoup + +from django.http.response import HttpResponse +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt +from django.dispatch import receiver +from django.db.models.signals import post_save +from django.contrib import messages + +from typing import Iterable + +from base.models import Announcement +from employee.models import Employee +from horilla.horilla_middlewares import _thread_locals +from notifications.signals import notify +from whatsapp.flows import ( + get_asset_category_flow_json, + get_attendance_request_json, + get_bonus_point_json, + get_leave_request_json, + get_reimbursement_request_json, + get_shift_request_json, + get_work_type_request_json, +) +from whatsapp.models import WhatsappCredientials +from whatsapp.utils import ( + asset_request_create, + attendance_request_create, + bonus_point_create, + create_flow, + create_help_message, + create_template_buttons, + create_welcome_message, + leave_request_create, + publish_flow, + reimbursement_create, + send_document_message, + send_flow_message, + send_template_message, + send_text_message, + send_image_message, + shift_create, + update_flow, + work_type_create, +) + + +DETAILED_FLOW = [ + { + "template_name": "leave", + "flow_name": "leave_request_flow", + "flow_json": get_leave_request_json(), + }, + { + "template_name": "shift", + "flow_name": "shift_request", + "flow_json": get_shift_request_json(), + }, + { + "template_name": "bonus_point", + "flow_name": "bonus_point_flow", + "flow_json": get_bonus_point_json(), + }, + { + "template_name": "reimbursement", + "flow_name": "reimbursement_flow", + "flow_json": get_reimbursement_request_json(), + }, + { + "template_name": "work_type", + "flow_name": "work_type_flow", + "flow_json": get_work_type_request_json(), + }, + { + "template_name": "attendance", + "flow_name": "attendance_flow", + "flow_json": get_attendance_request_json(), + }, + { + "template_name": "asset", + "flow_name": "asset_flow", + "flow_json": get_asset_category_flow_json(), + }, +] + +processed_messages = set() +logger = logging.getLogger(__name__) + + +def clean_string(s): + """ + Cleans a given string by removing punctuation and converting it to lowercase. + + Args: + s (str): The string to clean. + + Returns: + str: The cleaned string, or the original string if an error occurs. + """ + + try: + translator = str.maketrans("", "", string.punctuation + " _") + cleaned_string = s.translate(translator).lower() + return cleaned_string + except: + return s + + +@csrf_exempt +def whatsapp(request): + """ + Handles incoming WhatsApp webhook requests. + + Args: + request (HttpRequest): The incoming HTTP request. + + Returns: + HttpResponse: A response indicating the status of the operation. + """ + + if request.method == "GET": + credentials = WhatsappCredientials.objects.first() + + token = request.GET.get("hub.verify_token") + challenge = request.GET.get("hub.challenge") + if token == credentials.meta_webhook_token: + return HttpResponse(challenge, status=200) + + if request.method == "POST": + data = json.loads(request.body) + if "object" in data and "entry" in data: + if data["object"] == "whatsapp_business_account": + for entry in data["entry"]: + changes = entry.get("changes", [])[0] + value = changes.get("value", {}) + + if "messages" in value: + try: + metadata = value.get("metadata", {}) + contacts = value.get("contacts", [])[0] + messages = value.get("messages", [])[0] + + message_id = messages.get("id") + if message_id in processed_messages: + continue + + processed_messages.add(message_id) + + profile_name = contacts.get("profile", {}).get("name") + from_number = messages.get("from") + text = messages.get("text", {}).get("body") + type = messages.get("type", {}) + flow_response = ( + messages.get("interactive", {}) + .get("nfm_reply", {}) + .get("response_json", {}) + ) + + if type == "interactive": + flow_conversion(from_number, flow_response) + if type == "button": + text = messages.get("button", {}).get("text", {}) + + text = clean_string(text) + + # Handle different messages based on cleaned text + if text == "helloworld": + send_template_message(from_number, "hello_world") + elif text == "help": + send_template_message(from_number, "help_50") + elif text in ["asset", "assetrequest"]: + send_flow_message(from_number, "asset") + elif text in ["shift", "shiftrequest"]: + send_flow_message(from_number, "shift") + elif text in ["worktype", "worktyperequest"]: + send_flow_message(from_number, "work_type") + elif text in ["attendance", "attendancerequest"]: + send_flow_message(from_number, "attendance") + elif text in ["leave", "leaverequest"]: + send_flow_message(from_number, "leave") + elif text in ["reimbursement", "reimbursementrequest"]: + send_flow_message(from_number, "reimbursement") + elif text in ["bonus", "bonuspoint", "bonuspointredeem"]: + send_flow_message(from_number, "bonus_point") + elif text in [ + "hi", + "hello", + "goodmorning", + "goodafternoon", + "goodevening", + "goodnight", + "hlo", + ]: + send_template_message(from_number, "welcome_message_50") + elif text == "image": + try: + image_relative_url = ( + Employee.objects.filter(phone=from_number) + .first() + .employee_profile.url + ) + image_link = request.build_absolute_uri( + image_relative_url + ) + except Exception as e: + print(e) + + send_image_message(from_number, image_link) + elif text == "document": + try: + document_relative_url = ( + Employee.objects.filter(phone=from_number) + .first() + .employee_profile.url + ) + document_link = request.build_absolute_uri( + document_relative_url + ) + except Exception as e: + print(e) + + send_document_message(from_number, document_link) + elif text == "string": + send_text_message( + from_number, "test message", "test heading" + ) + else: + if text: + send_template_message( + from_number, "button_template_50" + ) + + except KeyError as e: + print(f"KeyError: {e}") + return HttpResponse("Bad Request", status=400) + except Exception as e: + print(f"Unexpected error: {e}") + return HttpResponse("Internal Server Error", status=500) + + return HttpResponse("Message processed", status=403) + + return HttpResponse("error", status=200) + + +def create_generic_templates(request,id): + """ + Creates generic message templates for WhatsApp. + + Args: + request (HttpRequest): The incoming HTTP request. + + Returns: + HttpResponse: A response indicating the success or failure of template creation. + """ + + try: + create_template_buttons(id) + create_welcome_message(id) + create_help_message(id) + create_flows(id) + + credential = WhatsappCredientials.objects.get(id=id) + credential.created_templates = True + credential.save() + + messages.success(request,"Message templates and flows created successfully.") + except: + messages.error(request,"Message templates and flows creation failed.") + return HttpResponse("") + + +@csrf_exempt +def create_flows(cred_id): + """ + Creates and publishes flows based on predefined details. + + Args: + request (HttpRequest): The incoming HTTP request. + + Returns: + HttpResponse: A response indicating the success or failure of flow creation. + """ + + try: + for flow in DETAILED_FLOW: + template_name = flow["template_name"] + flow_name = flow["flow_name"] + flow_json = flow["flow_json"] + credential = WhatsappCredientials.objects.get(id=cred_id) + + # Create flow + create_response = create_flow(flow_name, template_name, cred_id) + create_response_data = create_response.json() + + flow_id = create_response_data.get("id") + if not flow_id: + return HttpResponse( + json.dumps(create_response_data), + status=create_response.status_code, + content_type="application/json", + ) + + # Update flow + update_response = update_flow(flow_id, flow_json,credential.meta_token) + update_response_data = update_response.json() + if update_response_data.get("validation_error", {}): + return HttpResponse( + json.dumps(update_response_data), + status=update_response.status_code, + content_type="application/json", + ) + + # Publish flow + publish_response = publish_flow(flow_id,credential.meta_token) + publish_response_data = publish_response.json() + if publish_response_data.get("error", {}): + return HttpResponse( + json.dumps(publish_response_data), + status=publish_response.status_code, + content_type="application/json", + ) + + return HttpResponse( + json.dumps({"message": "Flow created successfully"}), + status=200, + content_type="application/json", + ) + + except Exception as e: + print(f"Unexpected error: {e}") + return HttpResponse( + json.dumps({"error": str(e)}), status=500, content_type="application/json" + ) + + +def send_notification_task(recipient, verb, redirect, icon): + """ + Background task to send a notification message via WhatsApp. + """ + try: + request = getattr(_thread_locals, "request", None) + link = request.build_absolute_uri(redirect) if redirect else None + message = f"{verb}\nFor more details, \n{link}." if link else verb + + recipients = ( + recipient + if isinstance(recipient, Iterable) and not isinstance(recipient, str) + else [recipient] + ) + + for user in recipients: + phone_number = user.employee_get.phone + if phone_number: + send_text_message(phone_number, message) + else: + print(f"No phone number available for recipient {user}") + + except Exception as e: + print(f"Error in notification task: {e}") + + +# @receiver(notify) +def send_notification_on_whatsapp(sender, recipient, verb, redirect, icon, **kwargs): + + thread = threading.Thread( + target=send_notification_task, args=(recipient, verb, redirect, icon) + ) + thread.start() + + +def send_announcement_task(instance, request): + """ + Background task to send an announcement message via WhatsApp. + """ + employees = instance.employees.all() + header = instance.title + body = instance.description + + soup = BeautifulSoup(body, "html.parser") + paragraphs = [] + for element in soup.find_all(["p", "ul", "ol", "h1", "h2", "h3", "h4", "h5", "h6"]): + if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]: + heading_text = element.get_text(strip=True).capitalize() + paragraphs.append(f"*{heading_text}*") + + elif element.name in ["ul", "ol"]: + list_items = [] + for li in element.find_all("li"): + item_text = [] + for child in li.children: + if child.name == "code": + item_text.append(f"`{child.get_text()}`") + elif child.name in ["strong", "b"]: + item_text.append(f"*{child.get_text()}*") + elif child.name == "span": + item_text.append(child.get_text()) + elif child.name == "a": + item_text.append(child.get_text()) + elif isinstance(child, str): + item_text.append(child.strip()) + list_items.append("• " + " ".join(item_text).strip()) + paragraphs.append("\n".join(list_items)) + + elif element.name == "p": + para_text = [] + for child in element.children: + if child.name == "code": + para_text.append(f"`{child.get_text()}`") + elif child.name in ["strong", "b"]: + para_text.append(f"*{child.get_text()}*") + elif child.name == "span": + para_text.append(child.get_text()) + elif child.name == "a": + para_text.append(child.get_text()) + elif isinstance(child, str): + para_text.append(child.strip()) + paragraphs.append(" ".join(para_text).strip()) + final_text = "\n\n".join(paragraphs) + + for employee in employees: + number = employee.phone + send_text_message(number, final_text, header) + for attachment in instance.attachments.all(): + link = attachment.file.url + document_link = request.build_absolute_uri(link) + send_document_message(number, document_link) + + +# @receiver(post_save, sender=Announcement) +def send_announcement_on_whatsapp(sender, instance, created, **kwargs): + if not created: + request = getattr(_thread_locals, "request", None) + + thread = threading.Thread( + target=send_announcement_task, args=(instance, request) + ) + thread.start() + + +def flow_conversion(number, flow_response_json): + """ + Processes a flow response based on the type of request. + + Args: + number (str): The phone number of the employee. + flow_response_json (str): The JSON response from the flow. + + Returns: + Response: The response from sending a message. + """ + + employee = Employee.objects.filter(phone=number).first() + flow_response = json.loads(flow_response_json) + message = "Something went wrong ......" + type = flow_response["type"] + + if type == "shift_request": + message = shift_create(employee, flow_response) + elif type == "leave_request": + message = leave_request_create(employee, flow_response) + elif type == "work_type": + message = work_type_create(employee, flow_response) + elif type == "asset_request": + message = asset_request_create(employee, flow_response) + elif type == "attendance_request": + message = attendance_request_create(employee, flow_response) + elif type == "bonus_point": + message = bonus_point_create(employee, flow_response) + elif type == "reimbursement": + message = reimbursement_create(employee, flow_response) + + response = send_text_message(number, message) + return response + + +def whatsapp_credential_view(request): + """ + Renders the WhatsApp credentials view. + + Args: + request (HttpRequest): The incoming HTTP request. + + Returns: + HttpResponse: The rendered credentials view. + """ + + return render(request, "whatsapp/credentials_view.html", {})