diff --git a/onboarding/filters.py b/onboarding/filters.py index 7d429aef5..db0e1df0b 100644 --- a/onboarding/filters.py +++ b/onboarding/filters.py @@ -7,6 +7,8 @@ from django import forms from django_filters import filters from base.filters import FilterSet +from base.models import Company +from employee.models import Employee from onboarding.models import CandidateStage, OnboardingStage from recruitment.models import Candidate @@ -36,6 +38,32 @@ class OnboardingStageFilter(FilterSet): field_name="stage_title", method="pipeline_search" ) + employee_id = filters.ModelChoiceFilter( + queryset=Employee.objects.all(), label="Stage Manager" + ) + + onboarding_task__task_title = filters.CharFilter( + field_name="onboarding_task__task_title", lookup_expr="icontains", label="Task" + ) + + onboarding_task__employee_id = filters.ModelChoiceFilter( + field_name="onboarding_task__employee_id", + queryset=Employee.objects.all(), + label="Task Manager", + ) + + recruitment_id__company_id = filters.ModelChoiceFilter( + field_name="recruitment_id__company_id", + queryset=Company.objects.all(), + label="Company", + ) + + onboarding_task__candidates = filters.ModelChoiceFilter( + field_name="onboarding_task__candidates", + queryset=Candidate.objects.all(), + label="Candidates", + ) + class Meta: model = OnboardingStage fields = "__all__" diff --git a/payroll/filters.py b/payroll/filters.py index f72712b57..3f8e0ff2d 100644 --- a/payroll/filters.py +++ b/payroll/filters.py @@ -279,6 +279,22 @@ class PayslipFilter(FilterSet): month = django_filters.CharFilter(field_name="start_date", lookup_expr="month") year = django_filters.CharFilter(field_name="start_date", lookup_expr="year") + allowance_title = django_filters.CharFilter( + method="filter_by_allowance_title", label="Allowance Title" + ) + allowance_amount_gte = django_filters.NumberFilter( + method="filter_by_allowance_amount_gte" + ) + allowance_amount_lte = django_filters.NumberFilter( + method="filter_by_allowance_amount_lte" + ) + deduction_amount_gte = django_filters.NumberFilter( + method="filter_by_deduction_amount_gte" + ) + deduction_amount_lte = django_filters.NumberFilter( + method="filter_by_deduction_amount_lte" + ) + class Meta: """ Meta class to add additional options @@ -298,8 +314,82 @@ class PayslipFilter(FilterSet): "net_pay__lte", "net_pay__gte", "sent_to_employee", + "allowance_amount_gte", + "allowance_amount_lte", + "deduction_amount_gte", + "deduction_amount_lte", ] + def filter_by_allowance_amount_gte(self, queryset, name, value): + return queryset.filter( + id__in=[ + p.id + for p in queryset + if any( + float(allowance.get("amount", 0)) >= float(value) + for allowance in (p.pay_head_data or {}).get("allowances", []) + ) + ] + ) + + def filter_by_allowance_amount_lte(self, queryset, name, value): + return queryset.filter( + id__in=[ + p.id + for p in queryset + if all( + float(allowance.get("amount", 0)) <= float(value) + for allowance in (p.pay_head_data or {}).get("allowances", []) + ) + ] + ) + + def filter_by_deduction_amount_lte(self, queryset, name, value): + value = float(value) + deduction_keys = [ + "pretax_deductions", + "gross_pay_deductions", + "basic_pay_deductions", + "post_tax_deductions", + "tax_deductions", + "net_deductions", + ] + + return queryset.filter( + id__in=[ + p.id + for p in queryset + if all( + float(d.get("amount", 0)) <= value + for key in deduction_keys + for d in (p.pay_head_data or {}).get(key, []) + ) + ] + ) + + def filter_by_deduction_amount_gte(self, queryset, name, value): + value = float(value) + deduction_keys = [ + "pretax_deductions", + "gross_pay_deductions", + "basic_pay_deductions", + "post_tax_deductions", + "tax_deductions", + "net_deductions", + ] + + return queryset.filter( + id__in=[ + p.id + for p in queryset + if any( + float(d.get("amount", 0)) >= value + for key in deduction_keys + for d in (p.pay_head_data or {}).get(key, []) + ) + ] + ) + def __init__(self, data=None, queryset=None, *, request=None, prefix=None): super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) for field in self.form.fields.keys(): diff --git a/recruitment/filters.py b/recruitment/filters.py index 9efb16bf0..361a55ca5 100644 --- a/recruitment/filters.py +++ b/recruitment/filters.py @@ -243,6 +243,7 @@ class RecruitmentFilter(FilterSet): "is_active", "is_published", "job_position_id", + "open_positions", ] def filter_by_name(self, queryset, _, value): diff --git a/report/__init__.py b/report/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/report/admin.py b/report/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/report/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/report/apps.py b/report/apps.py new file mode 100644 index 000000000..656d2d082 --- /dev/null +++ b/report/apps.py @@ -0,0 +1,18 @@ +from django.apps import AppConfig + + +class ReportConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'report' + + def ready(self) -> None: + ready = super().ready() + from django.urls import include, path + from horilla.urls import urlpatterns + from horilla.horilla_settings import APPS + + urlpatterns.append( + path("report/", include("report.urls")), + ) + + return ready \ No newline at end of file diff --git a/report/migrations/__init__.py b/report/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/report/models.py b/report/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/report/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/report/sidebar.py b/report/sidebar.py new file mode 100644 index 000000000..368de1da5 --- /dev/null +++ b/report/sidebar.py @@ -0,0 +1,90 @@ +from django.utils.translation import gettext_lazy as trans +from django.urls import reverse_lazy +from django.apps import apps + + +MENU = trans("Reports") +IMG_SRC = "images/ui/report.svg" +ACCESSIBILITY = "report.sidebar.menu_accessibility" + + +SUBMENUS = [ + + ] + +# Dynamically adding submenu if the app is installed +if apps.is_installed('recruitment'): + SUBMENUS.append({ + "menu": "Recruitment", + "redirect": reverse_lazy("recruitment-report"), + "accessibility": "report.sidebar.recruitment_accessibility", + }) + +if apps.is_installed('employee'): + SUBMENUS.append({ + "menu":"Employee", + "redirect":reverse_lazy("employee-report"), + "accessibility": "report.sidebar.employee_accessibility", + }) + +if apps.is_installed('attendance'): + SUBMENUS.append({ + "menu":"Attendance", + "redirect":reverse_lazy("attendance-report"), + "accessibility": "report.sidebar.attendance_accessibility", + }) + +if apps.is_installed('leave'): + SUBMENUS.append({ + "menu":"Leave", + "redirect":reverse_lazy("leave-report"), + "accessibility": "report.sidebar.leave_accessibility", + }) + +if apps.is_installed('payroll'): + SUBMENUS.append({ + "menu":"Payroll", + "redirect":reverse_lazy("payroll-report"), + "accessibility": "report.sidebar.payroll_accessibility", + }) + +if apps.is_installed('asset'): + SUBMENUS.append({ + "menu":"Asset", + "redirect":reverse_lazy("asset-report"), + "accessibility": "report.sidebar.asset_accessibility", + }) + +if apps.is_installed('pms'): + SUBMENUS.append({ + "menu":"Performance", + "redirect":reverse_lazy("pms-report"), + "accessibility": "report.sidebar.pms_accessibility", + }) + +def menu_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("recruitment.view_recruitment") or request.user.has_perm("employee.view_employee") or request.user.has_perm("pms.view_objective") or request.user.has_perm("attendance.view_attendance") or request.user.has_perm("leave.view_leaverequest") or request.user.has_perm("payroll.view_payslip") or request.user.has_perm("asset.view_asset") + + + +def recruitment_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("recruitment.view_recruitment") + +def employee_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("employee.view_employee") + +def attendance_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("attendance.view_attendance") + +def leave_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("leave.view_leaverequest") + +def payroll_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("payroll.view_payslip") + +def asset_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("asset.view_asset") + +def pms_accessibility(request, submenu, user_perms, *args, **kwargs): + return request.user.is_superuser or request.user.has_perm("pms.view_objective") + diff --git a/report/templates/report/asset_report.html b/report/templates/report/asset_report.html new file mode 100644 index 000000000..a716f153d --- /dev/null +++ b/report/templates/report/asset_report.html @@ -0,0 +1,381 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} +{% load widget_tweaks %} +{% load assets_custom_filter %} + + +
+ +
+

+ {% trans "Asset Reports" %} +

+ + +
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+
+ + + +
+
+ + + + + + +{% endblock content %} diff --git a/report/templates/report/attendance_report.html b/report/templates/report/attendance_report.html new file mode 100644 index 000000000..3b36a2e62 --- /dev/null +++ b/report/templates/report/attendance_report.html @@ -0,0 +1,524 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} + +
+ {% include 'filter_tags.html' %} + +
+

+ {% trans "Attendance Reports" %} +

+ + +
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+ +
+ + +
+
+ + + + + + + +{% endblock content %} diff --git a/report/templates/report/employee_report.html b/report/templates/report/employee_report.html new file mode 100644 index 000000000..1b1cb2d8d --- /dev/null +++ b/report/templates/report/employee_report.html @@ -0,0 +1,411 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} + +
+ +
+

+ {% trans "Employee Reports" %} +

+
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+
+ + +
+
+ + + + +{% endblock content %} diff --git a/report/templates/report/leave_report.html b/report/templates/report/leave_report.html new file mode 100644 index 000000000..5bde76d89 --- /dev/null +++ b/report/templates/report/leave_report.html @@ -0,0 +1,587 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} +{% include 'filter_tags.html' %} + +
+ +
+

+ {% trans "Leave Reports" %} +

+ + +
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+ + +
+ + + Choose Report : + + + +
+ + +
+
+ + + + + + +{% endblock content %} diff --git a/report/templates/report/payroll_report.html b/report/templates/report/payroll_report.html new file mode 100644 index 000000000..6500e3c2b --- /dev/null +++ b/report/templates/report/payroll_report.html @@ -0,0 +1,583 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} + +
+ +
+

+ {% trans "Payroll Reports" %} +

+ + +
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+
+ + + Choose Report : + + + +
+ + +
+
+ + + + + + +{% endblock content %} diff --git a/report/templates/report/pms_report.html b/report/templates/report/pms_report.html new file mode 100644 index 000000000..7c3a98611 --- /dev/null +++ b/report/templates/report/pms_report.html @@ -0,0 +1,526 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} + +
+ +
+

+ {% trans "Performance Reports" %} +

+ +
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + + + +
+
+ + + Choose Report : + + + + +
+ + + +
+ +
+ + + + + + +{% endblock content %} diff --git a/report/templates/report/recruitment_report.html b/report/templates/report/recruitment_report.html new file mode 100644 index 000000000..f37dde34f --- /dev/null +++ b/report/templates/report/recruitment_report.html @@ -0,0 +1,594 @@ +{% extends "index.html" %} +{% block content %} +{% load static %} +{% load i18n %} + +
+ +
+

+ {% trans "Candidate Reports" %} +

+
+ +
+ +
+ +
+
+ + +
+
+
+
+ + + + +
+
+ + + {% trans "Choose Report" %} : + + + + +
+ + + +
+
+ + + + + +{% endblock content %} diff --git a/report/tests.py b/report/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/report/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/report/urls.py b/report/urls.py new file mode 100644 index 000000000..8296004f0 --- /dev/null +++ b/report/urls.py @@ -0,0 +1,70 @@ + +from django.urls import path +from report.views import asset_report, employee_report,attendance_report,leave_report,payroll_report, pms_report,recruitment_report +from django.apps import apps + + + + +urlpatterns = [ + + path("employee-report",employee_report.employee_report,name='employee-report'), + path("employee-pivot",employee_report.employee_pivot,name='employee-pivot'), + +] + + +if apps.is_installed("recruitment"): + urlpatterns.extend( + [ + path("recruitment-report",recruitment_report.recruitment_report,name='recruitment-report'), + path("recruitment-pivot",recruitment_report.recruitment_pivot,name='recruitment-pivot'), + + ] + ) + +if apps.is_installed("attendance"): + urlpatterns.extend( + [ + path("attendance-report",attendance_report.attendance_report,name='attendance-report'), + path("attendance-pivot",attendance_report.attendance_pivot,name='attendance-pivot'), + + ] + ) + +if apps.is_installed("leave"): + urlpatterns.extend( + [ + path("leave-report",leave_report.leave_report, name="leave-report"), + path("leave-pivot",leave_report.leave_pivot,name='leave-pivot'), + + ] + ) + +if apps.is_installed("payroll"): + urlpatterns.extend( + [ + path("payroll-report",payroll_report.payroll_report, name="payroll-report"), + path("payroll-pivot",payroll_report.payroll_pivot,name='payroll-pivot'), + + ] + ) + +if apps.is_installed("asset"): + urlpatterns.extend( + [ + path("asset-report",asset_report.asset_report, name="asset-report"), + path("asset-pivot",asset_report.asset_pivot,name='asset-pivot'), + + ] + ) + +if apps.is_installed("pms"): + urlpatterns.extend( + [ + path("pms-report",pms_report.pms_report, name="pms-report"), + path("pms-pivot",pms_report.pms_pivot,name='pms-pivot'), + + ] + ) + diff --git a/report/views.py b/report/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/report/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/report/views/__init__.py b/report/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/report/views/asset_report.py b/report/views/asset_report.py new file mode 100644 index 000000000..ed861d580 --- /dev/null +++ b/report/views/asset_report.py @@ -0,0 +1,78 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.apps import apps + +if apps.is_installed("asset"): + + from asset.filters import AssetFilter + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from asset.models import Asset + + @login_required + @permission_required(perm="asset.view_asset") + def asset_report(request): + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + + asset_filter_form = AssetFilter() + + return render(request, "report/asset_report.html",{"company":company,"asset_filter_form": asset_filter_form.form,}) + + @login_required + @permission_required(perm="asset.view_asset") + def asset_pivot(request): + qs = Asset.objects.all() + + if asset_name := request.GET.get("asset_name"): + qs = qs.filter(asset_name = asset_name) + if asset_tracking_id := request.GET.get("asset_tracking_id"): + qs = qs.filter(asset_tracking_id = asset_tracking_id) + if asset_purchase_cost := request.GET.get("asset_purchase_cost"): + qs = qs.filter(asset_purchase_cost = asset_purchase_cost) + if asset_lot_number_id := request.GET.get("asset_lot_number_id"): + qs = qs.filter(asset_lot_number_id = asset_lot_number_id) + if asset_category_id := request.GET.get("asset_category_id"): + qs = qs.filter(asset_category_id = asset_category_id) + if asset_status := request.GET.get("asset_status"): + qs = qs.filter(asset_status = asset_status) + if asset_purchase_date := request.GET.get("asset_purchase_date"): + qs = qs.filter(asset_purchase_date = asset_purchase_date) + + data = list(qs.values( + "asset_name","asset_purchase_date","asset_tracking_id", + "asset_purchase_cost","asset_status","asset_category_id__asset_category_name","asset_lot_number_id__lot_number", + "expiry_date","assetassignment__assigned_by_employee_id__employee_work_info__department_id__department", + "assetassignment__assigned_by_employee_id__employee_work_info__job_position_id__job_position", + "assetassignment__assigned_by_employee_id__employee_work_info__job_role_id__job_role", + "assetassignment__assigned_by_employee_id__email","assetassignment__assigned_by_employee_id__phone", + "assetassignment__assigned_by_employee_id__gender","assetassignment__assigned_by_employee_id__employee_first_name", + "assetassignment__assigned_by_employee_id__employee_last_name","assetassignment__assigned_date", + "assetassignment__return_date","assetassignment__return_status", + + )) + data_list = [ + { + "Asset Name" : item["asset_name"], + "Asset User": f"{item['assetassignment__assigned_by_employee_id__employee_first_name']} {item['assetassignment__assigned_by_employee_id__employee_last_name']}" if item["assetassignment__assigned_by_employee_id__employee_first_name"] or item["assetassignment__assigned_by_employee_id__employee_last_name"] else "-", + "Email":item["assetassignment__assigned_by_employee_id__email"] if item["assetassignment__assigned_by_employee_id__email"] else "-", + "Phone":item["assetassignment__assigned_by_employee_id__phone"] if item["assetassignment__assigned_by_employee_id__phone"] else "-", + "Gender":item["assetassignment__assigned_by_employee_id__gender"] if item["assetassignment__assigned_by_employee_id__gender"] else "-", + "Department":item["assetassignment__assigned_by_employee_id__employee_work_info__department_id__department"] if item["assetassignment__assigned_by_employee_id__employee_work_info__department_id__department"] else "-", + "Job Position":item["assetassignment__assigned_by_employee_id__employee_work_info__job_position_id__job_position"] if item["assetassignment__assigned_by_employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role":item["assetassignment__assigned_by_employee_id__employee_work_info__job_role_id__job_role"] if item["assetassignment__assigned_by_employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Asset Purchce Date":item["asset_purchase_date"], + "Asset Cost":item["asset_purchase_cost"], + "Status":item["asset_status"], + "Assigned Date":item["assetassignment__assigned_date"] if item["assetassignment__assigned_date"] else "-", + "Return Date":item["assetassignment__return_date"] if item["assetassignment__return_date"] else "-", + "Return Condition":item["assetassignment__return_status"] if item["assetassignment__return_status"] else "-", + "Category":item["asset_category_id__asset_category_name"], + "Batch Number":item["asset_lot_number_id__lot_number"], + "Tracking ID":item["asset_tracking_id"], + "Expiry Date":item["expiry_date"] if item["expiry_date"] else "-", + }for item in data + ] + return JsonResponse(data_list, safe=False) diff --git a/report/views/attendance_report.py b/report/views/attendance_report.py new file mode 100644 index 000000000..d9d810102 --- /dev/null +++ b/report/views/attendance_report.py @@ -0,0 +1,139 @@ +from datetime import time,datetime + +from django.http import JsonResponse +from django.shortcuts import render + +from django.apps import apps + +if apps.is_installed("attendance"): + + from attendance.filters import AttendanceFilters + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from attendance.models import Attendance + + def convert_time_to_decimal_w(time_str): + try: + if isinstance(time_str, str): + hours, minutes = map(int, time_str.split(":")) + elif isinstance(time_str, time): + hours, minutes = time_str.hour, time_str.minute + else: + return "00.00" + + # Format as HH.MM + formatted_time = f"{hours:02}.{minutes:02}" + return formatted_time + except (ValueError, TypeError): + return "00.00" + + + + def convert_time_to_decimal(time_str): + """Format time as HH.MM for aggregation.""" + try: + if isinstance(time_str, str): # When time comes as string + t = datetime.strptime(time_str, "%H:%M:%S").time() + elif isinstance(time_str, time): + t = time_str + else: + return "00.00" + + # Format as HH.MM + formatted_time = f"{t.hour:02}.{t.minute:02}" + return formatted_time + except Exception: + return "00.00" + + @login_required + @permission_required(perm="attendance.view_attendance") + def attendance_report(request): + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + + return render(request, "report/attendance_report.html",{'company':company,"f": AttendanceFilters() }) + + @login_required + @permission_required(perm="attendance.view_attendance") + def attendance_pivot(request): + qs =Attendance.objects.all() + filter_obj = AttendanceFilters(request.GET, queryset=qs) + qs = filter_obj.qs + + data = list(qs.values( + 'employee_id__employee_first_name','employee_id__employee_last_name','attendance_date','attendance_clock_in','attendance_clock_out', + 'attendance_worked_hour','minimum_hour','attendance_overtime','at_work_second','work_type_id__work_type','shift_id__employee_shift', + 'attendance_day__day','employee_id__gender','employee_id__email','employee_id__phone','employee_id__employee_work_info__department_id__department', 'employee_id__employee_work_info__job_role_id__job_role', + 'employee_id__employee_work_info__job_position_id__job_position', 'employee_id__employee_work_info__employee_type_id__employee_type', + 'employee_id__employee_work_info__experience', 'batch_attendance_id__title','employee_id__employee_work_info__company_id__company', + )) + DAY = { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + } + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + data_list = [ + { + "Name": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}", + "Gender": choice_gender.get(item["employee_id__gender"]), + "Email": item["employee_id__email"], + "Phone": item["employee_id__phone"], + "Department": item["employee_id__employee_work_info__department_id__department"] if item["employee_id__employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_id__employee_work_info__job_position_id__job_position"] if item["employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_id__employee_work_info__job_role_id__job_role"] if item["employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["work_type_id__work_type"] if item["work_type_id__work_type"] else "-", + "Shift": item["shift_id__employee_shift"] if item["shift_id__employee_shift"] else "-", + "Experience": item["employee_id__employee_work_info__experience"], + "Attendance Date": item['attendance_date'], + "Attendance Day": DAY.get(item['attendance_day__day']), + "Clock-in": format_time(item['attendance_clock_in']), + "Clock-out": format_time(item['attendance_clock_out']), + "At Work": format_seconds_to_time(item['at_work_second']), + "Worked Hour": item['attendance_worked_hour'], + "Minimum Hour": item['minimum_hour'], + "Overtime": item['attendance_overtime'], + "Batch":item['batch_attendance_id__title'] if item['batch_attendance_id__title'] else "-", + "Company":item['employee_id__employee_work_info__company_id__company'], + + # For correct total + "Clock-in Decimal": convert_time_to_decimal(item["attendance_clock_in"]), + "Clock-out Decimal": convert_time_to_decimal(item["attendance_clock_out"]), + "At Work Decimal": convert_time_to_decimal_w(format_seconds_to_time(item['at_work_second'])), + "Worked Hour Decimal": convert_time_to_decimal_w(item["attendance_worked_hour"]), + "Minimum Hour Decimal": convert_time_to_decimal_w(item["minimum_hour"]), + "Overtime Decimal": convert_time_to_decimal_w(item['attendance_overtime']), + + } + for item in data + ] + return JsonResponse(data_list, safe=False) + + + # Helper function to format time + def format_time(time_value): + if isinstance(time_value, str): # In case time is string + time_value = datetime.strptime(time_value, "%H:%M:%S").time() + return time_value.strftime("%H:%M") if time_value else "" + + + def format_seconds_to_time(seconds): + """Convert seconds to HH:MM format.""" + try: + seconds = int(seconds) + hours, remainder = divmod(seconds, 3600) + minutes, _ = divmod(remainder, 60) + return f"{hours:02}:{minutes:02}" + except (ValueError, TypeError): + return "00:00" + diff --git a/report/views/employee_report.py b/report/views/employee_report.py new file mode 100644 index 000000000..f33a9541a --- /dev/null +++ b/report/views/employee_report.py @@ -0,0 +1,59 @@ +from django.http import JsonResponse +from django.shortcuts import render + +from base.models import Company +from employee.filters import EmployeeFilter +from horilla_views.cbv_methods import login_required, permission_required +from employee.models import Employee + + +@login_required +@permission_required(perm="employee.view_employee") +def employee_report(request): + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + + return render(request, "report/employee_report.html",{'company':company, "f":EmployeeFilter()}) + +@login_required +@permission_required(perm="employee.view_employee") +def employee_pivot(request): + qs = Employee.objects.all() + filtered_qs = EmployeeFilter(request.GET,queryset=qs) + qs = filtered_qs.qs + + data = list(qs.values( + 'employee_first_name','employee_last_name', 'gender','email','phone','employee_work_info__department_id__department','employee_work_info__job_position_id__job_position', + 'employee_work_info__job_role_id__job_role','employee_work_info__work_type_id__work_type','employee_work_info__shift_id__employee_shift','employee_work_info__employee_type_id__employee_type', + 'employee_work_info__reporting_manager_id__employee_first_name','employee_work_info__reporting_manager_id__employee_last_name','employee_work_info__company_id__company','employee_work_info__date_joining', + 'employee_work_info__experience' + )) + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + + # Transform data to match format + data_list = [ + { + "Name": f"{item['employee_first_name']} {item['employee_last_name']}", + "Gender": choice_gender.get(item["gender"]), + "Email": item["email"], + "Phone": item["phone"], + "Department": item["employee_work_info__department_id__department"] if item["employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_work_info__job_position_id__job_position"] if item["employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_work_info__job_role_id__job_role"] if item["employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["employee_work_info__work_type_id__work_type"] if item["employee_work_info__work_type_id__work_type"] else "-", + "Shift": item["employee_work_info__shift_id__employee_shift"] if item["employee_work_info__shift_id__employee_shift"] else "-", + "Employee Type": item["employee_work_info__employee_type_id__employee_type"] if item["employee_work_info__employee_type_id__employee_type"] else "-", + "Reporting Manager": f"{item['employee_work_info__reporting_manager_id__employee_first_name']} {item['employee_work_info__reporting_manager_id__employee_last_name']}" if item['employee_work_info__reporting_manager_id__employee_first_name'] else '-' , + "Date of Joining": item["employee_work_info__date_joining"] if item["employee_work_info__date_joining"] else '-', + "Experience": round(float(item["employee_work_info__experience"] or 0), 2), + "Company": item["employee_work_info__company_id__company"], + } + for item in data + ] + return JsonResponse(data_list, safe=False) diff --git a/report/views/leave_report.py b/report/views/leave_report.py new file mode 100644 index 000000000..fb3799550 --- /dev/null +++ b/report/views/leave_report.py @@ -0,0 +1,133 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.apps import apps + +if apps.is_installed("leave"): + + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from leave.filters import AssignedLeaveFilter, LeaveRequestFilter + from leave.models import AvailableLeave, LeaveRequest + + + @login_required + @permission_required(perm="leave.view_leaverequest") + def leave_report(request): + company = "all" + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id = selected_company).first() + + leave_request_filter = LeaveRequestFilter() + + return render(request, "report/leave_report.html",{'company' : company, "form": leave_request_filter.form, "f": AssignedLeaveFilter(),} ) + + @login_required + @permission_required(perm="leave.view_leaverequest") + def leave_pivot(request): + model_type = request.GET.get("model", "leave_request") # Default to LeaveRequest + + if model_type == "leave_request": + + qs = LeaveRequest.objects.all() + leave_filter = LeaveRequestFilter(request.GET, queryset=qs) + qs = leave_filter.qs + + data = list(qs.values( + "employee_id__employee_first_name","employee_id__employee_last_name","leave_type_id__name", + "start_date","start_date_breakdown","end_date","end_date_breakdown","requested_days","status", + 'employee_id__gender','employee_id__email','employee_id__phone','employee_id__employee_work_info__department_id__department', + 'employee_id__employee_work_info__job_role_id__job_role','employee_id__employee_work_info__job_position_id__job_position', + 'employee_id__employee_work_info__employee_type_id__employee_type','employee_id__employee_work_info__experience', + 'employee_id__employee_work_info__work_type_id__work_type','employee_id__employee_work_info__shift_id__employee_shift', + 'employee_id__employee_work_info__company_id__company' + + )) + BREAKDOWN_MAP = { + "full_day": "Full Day", + "first_half": "First Half", + "second_half": "Second Half", + } + + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + + LEAVE_STATUS = { + "requested": "Requested", + "approved": "Approved", + "cancelled": "Cancelled", + "rejected": "Rejected", + } + data_list = [ + { + "Name": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}", + "Gender": choice_gender.get(item["employee_id__gender"]), + "Email": item["employee_id__email"], + "Phone": item["employee_id__phone"], + "Department": item["employee_id__employee_work_info__department_id__department"] if item["employee_id__employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_id__employee_work_info__job_position_id__job_position"] if item["employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_id__employee_work_info__job_role_id__job_role"] if item["employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["employee_id__employee_work_info__work_type_id__work_type"] if item["employee_id__employee_work_info__work_type_id__work_type"] else "-", + "Shift": item["employee_id__employee_work_info__shift_id__employee_shift"] if item["employee_id__employee_work_info__shift_id__employee_shift"] else "-", + "Experience": item["employee_id__employee_work_info__experience"], + "Leave Type": item["leave_type_id__name"], + "Start Date": item["start_date"], + "Start Date Breakdown": BREAKDOWN_MAP.get(item["start_date_breakdown"], "-"), + "End Date Breakdown": BREAKDOWN_MAP.get(item["end_date_breakdown"], "-"), + "End Date": item["end_date"], + "Requested Days": item["requested_days"], + "Status": LEAVE_STATUS.get(item["status"]), + "Company":item['employee_id__employee_work_info__company_id__company'], + } + for item in data + ] + elif model_type == "available_leave": + + qs = AvailableLeave.objects.all() + available_leave_filter = AssignedLeaveFilter(request.GET, queryset= qs) + qs = available_leave_filter.qs + + data = list(qs.values( + "employee_id__employee_first_name","employee_id__employee_last_name","leave_type_id__name", + "available_days","carryforward_days","total_leave_days","assigned_date","reset_date","expired_date", + 'employee_id__gender','employee_id__email','employee_id__phone','employee_id__employee_work_info__department_id__department', + 'employee_id__employee_work_info__job_role_id__job_role','employee_id__employee_work_info__job_position_id__job_position', + 'employee_id__employee_work_info__employee_type_id__employee_type','employee_id__employee_work_info__experience', + 'employee_id__employee_work_info__work_type_id__work_type','employee_id__employee_work_info__shift_id__employee_shift', + 'employee_id__employee_work_info__company_id__company', + )) + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + data_list = [ + { + "Name": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}", + "Gender": choice_gender.get(item["employee_id__gender"]), + "Email": item["employee_id__email"], + "Phone": item["employee_id__phone"], + "Department": item["employee_id__employee_work_info__department_id__department"] if item["employee_id__employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_id__employee_work_info__job_position_id__job_position"] if item["employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_id__employee_work_info__job_role_id__job_role"] if item["employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["employee_id__employee_work_info__work_type_id__work_type"] if item["employee_id__employee_work_info__work_type_id__work_type"] else "-", + "Shift": item["employee_id__employee_work_info__shift_id__employee_shift"] if item["employee_id__employee_work_info__shift_id__employee_shift"] else "-", + "Experience": item["employee_id__employee_work_info__experience"], + "Leave Type": item["leave_type_id__name"], + "Available Days": item["available_days"], + "Carryforward Days": item["carryforward_days"], + "Total Leave Days": item["total_leave_days"], + "Assigned Date": item["assigned_date"], + "Reset Date": item.get("reset_date", "-") or "-", + "Expired Date": item.get("expired_date", "-") or "-", + "Company":item['employee_id__employee_work_info__company_id__company'], + } + for item in data + ] + else: + data_list = [] # Empty if invalid model selected + + return JsonResponse(data_list, safe=False) diff --git a/report/views/payroll_report.py b/report/views/payroll_report.py new file mode 100644 index 000000000..e3e317ef4 --- /dev/null +++ b/report/views/payroll_report.py @@ -0,0 +1,254 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.utils.dateparse import parse_date +from django.apps import apps + +if apps.is_installed("payroll"): + + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from payroll.filters import PayslipFilter + from payroll.models.models import Payslip + + + @login_required + @permission_required(perm="payroll.view_payslip") + def payroll_report(request): + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + + if request.user.has_perm("payroll.view_payslip"): + payslips = Payslip.objects.all() + else: + payslips = Payslip.objects.filter(employee_id__employee_user_id=request.user) + + filter_form = PayslipFilter(request.GET, payslips) + + return render(request, "report/payroll_report.html",{'company':company,"f":filter_form}) + + @login_required + @permission_required(perm="payroll.view_payslip") + def payroll_pivot(request): + model_type = request.GET.get("model", "payslip") + + if model_type == 'payslip': + qs = Payslip.objects.all() + + if employee_id := request.GET.getlist("employee_id"): + qs = qs.filter(employee_id__id__in=employee_id) + if status := request.GET.get("status"): + qs = qs.filter(status=status) + if group_name := request.GET.get("group_name"): + qs = qs.filter(group_name=group_name) + + start_date_from = parse_date(request.GET.get("start_date_from", "")) + start_date_to = parse_date(request.GET.get("start_date_till", "")) + if start_date_from: + qs = qs.filter(start_date__gte=start_date_from) + if start_date_to: + qs = qs.filter(start_date__lte=start_date_to) + + end_date_from = parse_date(request.GET.get("end_date_from", "")) + end_date_to = parse_date(request.GET.get("end_date_till", "")) + if end_date_from: + qs = qs.filter(end_date__gte=end_date_from) + if end_date_to: + qs = qs.filter(end_date__lte=end_date_to) + + # Gross Pay Range + gross_pay_gte = request.GET.get("gross_pay__gte") + gross_pay_lte = request.GET.get("gross_pay__lte") + if gross_pay_gte: + qs = qs.filter(gross_pay__gte=gross_pay_gte) + if gross_pay_lte: + qs = qs.filter(gross_pay__lte=gross_pay_lte) + + # Deduction Range + deduction_gte = request.GET.get("deduction__gte") + deduction_lte = request.GET.get("deduction__lte") + if deduction_gte: + qs = qs.filter(deduction__gte=deduction_gte) + if deduction_lte: + qs = qs.filter(deduction__lte=deduction_lte) + + # Net Pay Range + net_pay_gte = request.GET.get("net_pay__gte") + net_pay_lte = request.GET.get("net_pay__lte") + if net_pay_gte: + qs = qs.filter(net_pay__gte=net_pay_gte) + if net_pay_lte: + qs = qs.filter(net_pay__lte=net_pay_lte) + + + data = list(qs.values( + 'id', # Include payslip ID to fetch pay_head_data later + 'employee_id__employee_first_name', 'employee_id__employee_last_name', 'employee_id__gender', 'employee_id__email', + 'employee_id__phone', 'start_date', 'end_date', 'contract_wage', 'basic_pay', 'gross_pay', 'deduction', 'net_pay','group_name', + 'status', 'employee_id__employee_work_info__department_id__department', 'employee_id__employee_work_info__job_role_id__job_role', + 'employee_id__employee_work_info__job_position_id__job_position', 'employee_id__employee_work_info__work_type_id__work_type', + 'employee_id__employee_work_info__shift_id__employee_shift', 'employee_id__employee_work_info__employee_type_id__employee_type', + 'employee_id__employee_work_info__experience', + )) + + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + + STATUS = { + "draft": "Draft", + "review_ongoing": "Review Ongoing", + "confirmed": "Confirmed", + "paid": "Paid" + } + + # Fetch pay_head_data separately and map by payslip ID + payslip_ids = [item["id"] for item in data] + pay_head_data_dict = dict(Payslip.objects.filter(id__in=payslip_ids).values_list("id", "pay_head_data")) + + data_list = [] + for item in data: + # Load pay_head_data for current payslip + pay_head_data = pay_head_data_dict.get(item["id"], {}) + + # Extract allowances and deductions + allowances = pay_head_data.get("allowances", []) + deductions = ( + pay_head_data.get("pretax_deductions", []) + pay_head_data.get("post_tax_deductions", []) + ) + + # Prepare allowance and deduction lists with properly rounded amounts + allowance_titles = ", ".join([allowance["title"] for allowance in allowances]) or "-" + allowance_amounts = ", ".join( + [str(round(float(allowance["amount"] or 0), 2)) for allowance in allowances] + ) or "-" + + deduction_titles = ", ".join([deduction["title"] for deduction in deductions]) or "-" + deduction_amounts = ", ".join( + [str(round(float(deduction["amount"] or 0), 2)) for deduction in deductions] + ) or "-" + + # Calculate total allowance amount + total_allowance_amount = sum( + [round(float(allowance["amount"] or 0), 2) for allowance in allowances] + ) + + # Calculate total deduction amount + total_deduction_amount = sum( + [round(float(deduction["amount"] or 0), 2) for deduction in deductions] + ) + + # Main data structure + data_list.append({ + "Employee": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}", + "Gender": choice_gender.get(item["employee_id__gender"]), + "Email": item["employee_id__email"], + "Phone": item["employee_id__phone"], + "Department": item["employee_id__employee_work_info__department_id__department"] if item["employee_id__employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_id__employee_work_info__job_position_id__job_position"] if item["employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_id__employee_work_info__job_role_id__job_role"] if item["employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["employee_id__employee_work_info__work_type_id__work_type"] if item["employee_id__employee_work_info__work_type_id__work_type"] else "-", + "Shift": item["employee_id__employee_work_info__shift_id__employee_shift"] if item["employee_id__employee_work_info__shift_id__employee_shift"] else "-", + "Employee Type": item["employee_id__employee_work_info__employee_type_id__employee_type"] if item["employee_id__employee_work_info__employee_type_id__employee_type"] else "-", + "Payslip Start Date": item["start_date"], + "Payslip End Date": item["end_date"], + "Batch Name": item['group_name'] if item['group_name'] else '-', + "Contract Wage": round(float(item["contract_wage"] or 0), 2), + "Basic Salary": round(float(item["basic_pay"] or 0), 2), + "Gross Pay": round(float(item["gross_pay"] or 0), 2), + "Net Pay": round(float(item["net_pay"] or 0), 2), + "Allowance Title": allowance_titles, + "Allowance Amount": allowance_amounts, + "Total Allowance Amount": round(total_allowance_amount, 2), + "Deduction Title": deduction_titles, + "Deduction Amount": deduction_amounts, + "Total Deduction Amount": round(total_deduction_amount, 2), + "Status": STATUS.get(item["status"]), + "Experience": round(float(item["employee_id__employee_work_info__experience"] or 0), 2), + }) + + elif model_type == "allowance": + + payslips = Payslip.objects.all() + + payslip_filter = PayslipFilter(request.GET, queryset=payslips) + filtered_qs = payslip_filter.qs # This uses all custom filters you defined + + data = list(filtered_qs.values( + 'id', # Include payslip ID to fetch pay_head_data later + 'employee_id__employee_first_name', 'employee_id__employee_last_name', 'employee_id__gender', 'employee_id__email', + 'employee_id__phone', 'start_date', 'end_date','status', 'employee_id__employee_work_info__department_id__department', + 'employee_id__employee_work_info__job_role_id__job_role','employee_id__employee_work_info__job_position_id__job_position', + 'employee_id__employee_work_info__work_type_id__work_type','employee_id__employee_work_info__shift_id__employee_shift', + )) + + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + + STATUS = { + "draft": "Draft", + "review_ongoing": "Review Ongoing", + "confirmed": "Confirmed", + "paid": "Paid" + } + + # Fetch pay_head_data separately and map by payslip ID + payslip_ids = [item["id"] for item in data] + pay_head_data_dict = dict(Payslip.objects.filter(id__in=payslip_ids).values_list("id", "pay_head_data")) + + data_list = [] + for item in data: + # Load pay_head_data for current payslip + pay_head_data = pay_head_data_dict.get(item["id"], {}) + + # Combine Allowances and Deductions in a single section + all_pay_data = [] + + # Add Allowances to combined data + for allowance in pay_head_data.get("allowances", []): + all_pay_data.append({ + "Pay Type": "Allowance", + "Title": allowance["title"], + "Amount": round(float(allowance["amount"] or 0), 2), + }) + + # Add Deductions to combined data + for deduction in ( + pay_head_data.get("pretax_deductions", []) + pay_head_data.get("post_tax_deductions", []) + ): + all_pay_data.append({ + "Pay Type": "Deduction", + "Title": deduction["title"], + "Amount": round(float(deduction["amount"] or 0), 2), + }) + + # Add combined data to main data list + for pay_item in all_pay_data: + data_list.append({ + "Employee": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}", + "Gender": choice_gender.get(item["employee_id__gender"]), + "Email": item["employee_id__email"], + "Phone": item["employee_id__phone"], + "Department": item["employee_id__employee_work_info__department_id__department"] if item["employee_id__employee_work_info__department_id__department"] else "-", + "Job Position": item["employee_id__employee_work_info__job_position_id__job_position"] if item["employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role": item["employee_id__employee_work_info__job_role_id__job_role"] if item["employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Work Type": item["employee_id__employee_work_info__work_type_id__work_type"] if item["employee_id__employee_work_info__work_type_id__work_type"] else "-", + "Shift": item["employee_id__employee_work_info__shift_id__employee_shift"] if item["employee_id__employee_work_info__shift_id__employee_shift"] else "-", + "Payslip Start Date": item["start_date"], + "Payslip End Date": item["end_date"], + "Allowance & Deduction": pay_item["Pay Type"], + "Allowance & Deduction Title": pay_item["Title"], + "Allowance & Deduction Amount": pay_item["Amount"], + "Status": STATUS.get(item["status"]), + }) + else: + data_list = [] + + return JsonResponse(data_list, safe=False) + diff --git a/report/views/pms_report.py b/report/views/pms_report.py new file mode 100644 index 000000000..29d7027ca --- /dev/null +++ b/report/views/pms_report.py @@ -0,0 +1,254 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.apps import apps + +if apps.is_installed("pms"): + + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from pms.filters import EmployeeObjectiveFilter, FeedbackFilter + from pms.models import EmployeeKeyResult, EmployeeObjective, Feedback, Objective + from pms.views import objective_filter_pagination + + + @login_required + @permission_required(perm="pms.view_objective") + def pms_report(request): + + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + employee = request.user.employee_get + objective_own = EmployeeObjective.objects.filter( + employee_id=employee, archive=False + ) + objective_own = objective_own.distinct() + + feedback = request.GET.get("search") # if the search is none the filter will works + if feedback is None: + feedback = "" + self_feedback = Feedback.objects.filter(employee_id=employee).filter( + review_cycle__icontains=feedback + ) + initial_data = {"archive": False} + feedback_filter_own = FeedbackFilter( + request.GET or initial_data, queryset=self_feedback + ) + + context = objective_filter_pagination(request, objective_own) + cm = {'company':company,"feedback_filter_form":feedback_filter_own.form,"emp_obj_form": EmployeeObjectiveFilter()} + context.update(cm) + + return render(request, "report/pms_report.html",context) + + @login_required + @permission_required(perm="pms.view_objective") + def pms_pivot(request): + + model_type = request.GET.get('model', 'objective') + if model_type == 'objective': + qs = Objective.objects.all() + + if managers := request.GET.getlist("managers"): + qs = qs.filter(managers__id__in=managers) + if assignees := request.GET.getlist("assignees"): + qs = qs.filter(assignees__id__in=assignees) + if duration := request.GET.get("duration"): + qs = qs.filter(duration=duration) + if key_result_id := request.GET.get("employee_objective__key_result_id"): + qs = qs.filter(key_result_id=key_result_id) + + data = list(qs.values( + "title","managers__employee_first_name","managers__employee_last_name", + "assignees__employee_first_name","assignees__employee_last_name", + "key_result_id__title","key_result_id__target_value","duration_unit","duration", + "company_id__company","key_result_id__progress_type","key_result_id__duration", + 'assignees__employee_work_info__department_id__department', 'assignees__employee_work_info__job_role_id__job_role', + 'assignees__employee_work_info__job_position_id__job_position', + )) + DURATION_UNIT = { + "days" :"Days", + "months" :"Months", + "years" :"Years", + } + KEY_RESULT_TARGET = { + "%" :"%", + "#" :"Number", + "Currency" :"Currency", + } + data_list = [ + { + "Objective":item["title"], + "Objective Duration":f'{item["duration"]} {DURATION_UNIT.get(item["duration_unit"])}', + "Manager":f"{item['managers__employee_first_name']} {item['managers__employee_last_name']}" if item['managers__employee_first_name'] else "-", + "Assignees":f"{item['assignees__employee_first_name']} {item['assignees__employee_last_name']}", + "Assignee Department":item["assignees__employee_work_info__department_id__department"] if item["assignees__employee_work_info__department_id__department"] else "-", + "Assignee Job Position":item["assignees__employee_work_info__job_position_id__job_position"] if item["assignees__employee_work_info__job_position_id__job_position"] else "-", + "Assignee Job Role":item["assignees__employee_work_info__job_role_id__job_role"] if item["assignees__employee_work_info__job_role_id__job_role"] else"-", + "Key Results":item["key_result_id__title"], + "Key Result Duration":f'{item["key_result_id__duration"]} {"Days"}', + "Key Result Target":f'{item["key_result_id__target_value"]} {KEY_RESULT_TARGET.get(item["key_result_id__progress_type"])}', + "Company":item["company_id__company"] + + }for item in data + ] + elif model_type == 'feedback': + + data_list = [] + + PERIOD = { + "days": "Days", + "months": "Months", + "years": "Years", + } + + feedbacks = Feedback.objects.select_related( + "manager_id", "employee_id", "question_template_id" + ).prefetch_related( + "colleague_id", "subordinate_id", + "question_template_id__question", + "feedback_answer__question_id", # related_name + "feedback_answer__employee_id", + ) + + + # ✅ FILTERS added here + if review_cycle := request.GET.get("review_cycle"): + feedbacks = feedbacks.filter(review_cycle=review_cycle) + if status := request.GET.get("status"): + feedbacks = feedbacks.filter(status=status) + if employee_id := request.GET.get("employee_id"): + feedbacks = feedbacks.filter(employee_id=employee_id) + if manager_id := request.GET.get("manager_id"): + feedbacks = feedbacks.filter(manager_id=manager_id) + if colleague_id := request.GET.get("colleague_id"): + feedbacks = feedbacks.filter(colleague_id=colleague_id) + if subordinate_id := request.GET.get("subordinate_id"): + feedbacks = feedbacks.filter(subordinate_id=subordinate_id) + if start_date := request.GET.get("start_date"): + feedbacks = feedbacks.filter(created_at__date__gte=start_date) + if end_date := request.GET.get("end_date"): + feedbacks = feedbacks.filter(created_at__date__lte=end_date) + + + for feedback in feedbacks: + manager = f"{feedback.manager_id.employee_first_name} {feedback.manager_id.employee_last_name}" if feedback.manager_id else "" + employee = f"{feedback.employee_id.employee_first_name} {feedback.employee_id.employee_last_name}" if feedback.employee_id else "" + + answerable_employees = list(feedback.colleague_id.all()) + list(feedback.subordinate_id.all()) + answerable_names = ', '.join( + f"{e.employee_first_name} {e.employee_last_name}" for e in answerable_employees + ) or "-" + + questions = feedback.question_template_id.question.all() + + # Fetch ALL answers for this feedback and map them grouped by question + answers = feedback.feedback_answer.select_related("employee_id", "question_id") + + for question in questions: + question_answers = [ans for ans in answers if ans.question_id_id == question.id] + + # If no one answered this question, still show the question + if not question_answers: + data_list.append({ + "Title":feedback.review_cycle, + "Manager": manager, + "Employee": employee, + "Answerable Employees": answerable_names, + "Questions": question.question, + "Answer": "", + "Answered Employees": "-", + "Status": feedback.status, + "Start Date": feedback.start_date, + "End Date": feedback.end_date, + "Is Cyclic": "Yes" if feedback.cyclic_feedback else "No", + "Cycle Period": f"{feedback.cyclic_feedback_days_count} {PERIOD.get(feedback.cyclic_feedback_period)}" if feedback.cyclic_feedback_days_count else "-" + }) + else: + for answer in question_answers: + answer_value = answer.answer.get("answer") if answer.answer else "" + answered_by = f"{answer.employee_id.employee_first_name} {answer.employee_id.employee_last_name}" if answer.employee_id else "-" + data_list.append({ + "Title":feedback.review_cycle, + "Manager": manager, + "Employee": employee, + "Answerable Employees": answerable_names, + "Questions": question.question, + "Answer": answer_value, + "Answered Employees": answered_by, + "Status": feedback.status, + "Start Date": feedback.start_date, + "End Date": feedback.end_date, + "Is Cyclic": "Yes" if feedback.cyclic_feedback else "No", + "Cycle Period": f"{feedback.cyclic_feedback_days_count} {PERIOD.get(feedback.cyclic_feedback_period)}" if feedback.cyclic_feedback_days_count else "-" + }) + elif model_type == 'employeeobjective': + + from django.utils.dateparse import parse_date + + qs=EmployeeKeyResult.objects.all() + + # Filter section + if assignees := request.GET.getlist("employee_id"): + qs = qs.filter(employee_objective_id__employee_id__id__in=assignees) + if key_result_id := request.GET.get("key_result_id"): + qs = qs.filter(key_result_id__id=key_result_id) + if status := request.GET.get("status"): + qs = qs.filter(status=status) + + start_date_from = parse_date(request.GET.get("start_date_from", "")) + start_date_to = parse_date(request.GET.get("start_date_till", "")) + if start_date_from: + qs = qs.filter(start_date__gte=start_date_from) + if start_date_to: + qs = qs.filter(start_date__lte=start_date_to) + + end_date_from = parse_date(request.GET.get("end_date_from", "")) + end_date_to = parse_date(request.GET.get("end_date_till", "")) + if end_date_from: + qs = qs.filter(end_date__gte=end_date_from) + if end_date_to: + qs = qs.filter(end_date__lte=end_date_to) + + data = list(qs.values( + "key_result","employee_objective_id__employee_id__employee_first_name","employee_objective_id__employee_id__employee_last_name", + "employee_objective_id__objective_id__title","employee_objective_id__objective_id__duration_unit","employee_objective_id__objective_id__duration", + "start_value","current_value","target_value","start_date","end_date","status","progress_type",'employee_objective_id__employee_id__employee_work_info__department_id__department', + 'employee_objective_id__employee_id__employee_work_info__job_role_id__job_role','employee_objective_id__employee_id__employee_work_info__job_position_id__job_position', + + )) + DURATION_UNIT = { + "days" :"Days", + "months" :"Months", + "years" :"Years", + } + KEY_RESULT_TARGET = { + "%" :"%", + "#" :"Number", + "Currency" :"Currency", + } + + data_list = [ + { + "Employee": f"{item['employee_objective_id__employee_id__employee_first_name']} {item['employee_objective_id__employee_id__employee_last_name']}", + "Department":item["employee_objective_id__employee_id__employee_work_info__department_id__department"] if item["employee_objective_id__employee_id__employee_work_info__department_id__department"] else "-", + "Job Position":item["employee_objective_id__employee_id__employee_work_info__job_position_id__job_position"] if item["employee_objective_id__employee_id__employee_work_info__job_position_id__job_position"] else "-", + "Job Role":item["employee_objective_id__employee_id__employee_work_info__job_role_id__job_role"] if item["employee_objective_id__employee_id__employee_work_info__job_role_id__job_role"] else "-", + "Employee Keyresult":item["key_result"], + "Objective":item["employee_objective_id__objective_id__title"], + "Objective Duration":f'{item["employee_objective_id__objective_id__duration"]} {DURATION_UNIT.get(item["employee_objective_id__objective_id__duration_unit"])}', + "Keyresult Start Value":f'{item["start_value"]} {KEY_RESULT_TARGET.get(item["progress_type"])}', + "Keyresult Target Value":f'{item["target_value"]} {KEY_RESULT_TARGET.get(item["progress_type"])}', + "Keyresult Current Value":f'{item["current_value"]} {KEY_RESULT_TARGET.get(item["progress_type"])}' if item["current_value"] else "-", + "Keyresult Start Date":item["start_date"] if item["start_date"] else "-", + "Keyresult End Date":item["end_date"] if item["end_date"] else "-", + "status":item["status"], + + }for item in data + ] + + else: + data_list =[] + + return JsonResponse(data_list, safe = False) diff --git a/report/views/recruitment_report.py b/report/views/recruitment_report.py new file mode 100644 index 000000000..b9d557845 --- /dev/null +++ b/report/views/recruitment_report.py @@ -0,0 +1,127 @@ +from django.http import JsonResponse +from django.shortcuts import render +from django.apps import apps + +if apps.is_installed("recruitment"): + + from base.models import Company + from horilla_views.cbv_methods import login_required, permission_required + from onboarding.filters import OnboardingStageFilter + from onboarding.models import OnboardingStage + from recruitment.filters import CandidateFilter, RecruitmentFilter + from recruitment.models import Candidate, Recruitment + + @login_required + @permission_required(perm="recruitment.view_recruitment") + def recruitment_report(request): + company = 'all' + selected_company = request.session.get("selected_company") + if selected_company != 'all': + company = Company.objects.filter(id=selected_company).first() + return render(request, "report/recruitment_report.html",{'company':company, "f":CandidateFilter(), "fr":RecruitmentFilter(),"fo":OnboardingStageFilter()}) + + @login_required + @permission_required(perm="recruitment.view_recruitment") + def recruitment_pivot(request): + model_type = request.GET.get("model", "candidate") # Default to Candidate + + if model_type == "candidate": + qs = Candidate.objects.all() + filter_obj = CandidateFilter(request.GET, queryset=qs) + qs = filter_obj.qs + + data = list(qs.values( + "name","recruitment_id__title","job_position_id__job_position","stage_id__stage","email","mobile", + "gender","offer_letter_status","recruitment_id__closed","recruitment_id__vacancy","country", + "recruitment_id__company_id__company","address","dob","state","city","source","job_position_id__department_id__department" + )) + choice_gender = { + "male": "Male", + "female": "Female", + "other": "Other", + } + OFFER_LETTER_STATUS = { + "not_sent" : "Not Sent", + "sent" : "Sent", + "accepted" : "Accepted", + "rejected" : "Rejected", + "joined" : "Joined", + } + SOURCE_CHOICE = { + "application":"Application Form", + "software":"Inside Software", + "other":"Other", + } + + + data_list = [ + { + "Candidate":item["name"], + "Email":item["email"], + "Phone":item["mobile"], + "Gender":choice_gender.get(item["gender"]), + "Address":item["address"], + "Date Of Birth":item["dob"], + "Country":item["country"] if item["country"] else "-", + "State":item["state"] if item["state"] else "-", + "City":item["city"] if item["city"] else "-", + "Source":SOURCE_CHOICE.get(item["source"]) if item["source"] else "-", + "Job Position":item["job_position_id__job_position"], + "Department":item["job_position_id__department_id__department"], + "Offer Letter":OFFER_LETTER_STATUS.get(item["offer_letter_status"]), + "Recruitment":item["recruitment_id__title"], + "Current Stage":item["stage_id__stage"], + "Recruitment Status": 'Closed' if item["recruitment_id__closed"] else 'Open', + "Vacancy" : item["recruitment_id__vacancy"], + "Company" : item["recruitment_id__company_id__company"], + + + }for item in data + ] + elif model_type == "recruitment": + qs = Recruitment.objects.all() + filter_obj = RecruitmentFilter(request.GET, queryset = qs) + qs = filter_obj.qs + data = list(qs.values( + "title","vacancy","closed","open_positions__job_position","start_date","end_date","is_published", + "recruitment_managers__employee_first_name","recruitment_managers__employee_last_name","company_id__company", + )) + data_list = [ + { + "Recruitment" : item["title"], + "Manager": f"{item['recruitment_managers__employee_first_name']} {item['recruitment_managers__employee_last_name']}", + "Is Closed": 'Closed' if item["closed"] else 'Open', + "Status": 'Published' if item["is_published"] else 'Not Published', + "Start Date":item["start_date"], + "End Date":item["end_date"], + "Job Position":item["open_positions__job_position"], + "Vacancy":item["vacancy"], + "Company" : item["company_id__company"], + }for item in data + ] + elif model_type == "onboarding": + qs = OnboardingStage.objects.all() + filter_obj = OnboardingStageFilter(request.GET, queryset = qs) + qs = filter_obj.qs + + data = list(qs.values( + "stage_title","recruitment_id__title","employee_id__employee_first_name","employee_id__employee_last_name", + "onboarding_task__task_title", + "onboarding_task__employee_id__employee_first_name","onboarding_task__employee_id__employee_last_name", + "onboarding_task__candidates__name","recruitment_id__company_id__company", + )) + + data_list = [ + { + "Recruitment": item["recruitment_id__title"], + "Stage": item["stage_title"], + "Stage Manager": f"{item['employee_id__employee_first_name']} {item['employee_id__employee_last_name']}" if item['employee_id__employee_first_name'] else "-", + "Task": item["onboarding_task__task_title"] if item["onboarding_task__task_title"] else "-", + "Task Manager": f"{item['onboarding_task__employee_id__employee_first_name']} {item['onboarding_task__employee_id__employee_last_name']}" if item['onboarding_task__employee_id__employee_first_name'] else "-", + "Candidates": item["onboarding_task__candidates__name"] if item["onboarding_task__candidates__name"] else "-", + "Company" : item["recruitment_id__company_id__company"] if item["recruitment_id__company_id__company"] else "-", + }for item in data + ] + else: + data_list = [] + return JsonResponse(data_list, safe=False) diff --git a/static/build/css/pivottable.min.css b/static/build/css/pivottable.min.css new file mode 100644 index 000000000..5237d272c --- /dev/null +++ b/static/build/css/pivottable.min.css @@ -0,0 +1,2 @@ +.pvtUi{color:#333}table.pvtTable{font-size:8pt;text-align:left;border-collapse:collapse}table.pvtTable tbody tr th,table.pvtTable thead tr th{background-color:#e6EEEE;border:1px solid #CDCDCD;font-size:8pt;padding:5px}table.pvtTable .pvtColLabel{text-align:center}table.pvtTable .pvtTotalLabel{text-align:right}table.pvtTable tbody tr td{color:#3D3D3D;padding:5px;background-color:#FFF;border:1px solid #CDCDCD;vertical-align:top;text-align:right}.pvtGrandTotal,.pvtTotal{font-weight:700}.pvtVals{text-align:center;white-space:nowrap}.pvtColOrder,.pvtRowOrder{cursor:pointer;width:15px;margin-left:5px;display:inline-block}.pvtAggregator{margin-bottom:5px}.pvtAxisContainer,.pvtVals{border:1px solid gray;background:#EEE;padding:5px;min-width:20px;min-height:20px;user-select:none;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}.pvtAxisContainer li{padding:8px 6px;list-style-type:none;cursor:move}.pvtAxisContainer li.pvtPlaceholder{-webkit-border-radius:5px;padding:3px 15px;-moz-border-radius:5px;border-radius:5px;border:1px dashed #aaa}.pvtAxisContainer li span.pvtAttr{-webkit-text-size-adjust:100%;background:#F3F3F3;border:1px solid #DEDEDE;padding:2px 5px;white-space:nowrap;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.pvtTriangle{cursor:pointer;color:grey}.pvtHorizList li{display:inline}.pvtVertList{vertical-align:top}.pvtFilteredAttribute{font-style:italic}.pvtFilterBox{z-index:100;width:300px;border:1px solid gray;background-color:#fff;position:absolute;text-align:center}.pvtFilterBox h4{margin:15px}.pvtFilterBox p{margin:10px auto}.pvtFilterBox label{font-weight:400}.pvtFilterBox input[type=checkbox]{margin-right:10px;margin-left:10px}.pvtFilterBox input[type=text]{width:230px}.pvtFilterBox .count{color:gray;font-weight:400;margin-left:3px}.pvtCheckContainer{text-align:left;font-size:14px;white-space:nowrap;overflow-y:scroll;width:100%;max-height:250px;border-top:1px solid #d3d3d3;border-bottom:1px solid #d3d3d3}.pvtCheckContainer p{margin:5px}.pvtRendererArea{padding:5px} + diff --git a/static/build/js/pivottable.min.js b/static/build/js/pivottable.min.js new file mode 100644 index 000000000..0537a21e6 --- /dev/null +++ b/static/build/js/pivottable.min.js @@ -0,0 +1,2 @@ +(function(){var t,e=[].indexOf||function(t){for(var e=0,n=this.length;e1?n+a[1]:"",r=/(\d+)(\d{3})/;r.test(o);)o=o.replace(r,"$1"+e+"$2");return o+i},m=function(e){var n;return n={digitsAfterDecimal:2,scaler:1,thousandsSep:",",decimalSep:".",prefix:"",suffix:""},e=t.extend({},n,e),function(t){var n;return isNaN(t)||!isFinite(t)?"":(n=i((e.scaler*t).toFixed(e.digitsAfterDecimal),e.thousandsSep,e.decimalSep),""+e.prefix+n+e.suffix)}},A=m(),x=m({digitsAfterDecimal:0}),S=m({digitsAfterDecimal:1,scaler:100,suffix:"%"}),l={count:function(t){return null==t&&(t=x),function(){return function(e,n,r){return{count:0,push:function(){return this.count++},value:function(){return this.count},format:t}}}},uniques:function(t,n){return null==n&&(n=x),function(r){var a;return a=r[0],function(r,o,i){return{uniq:[],push:function(t){var n;if(n=t[a],e.call(this.uniq,n)<0)return this.uniq.push(t[a])},value:function(){return t(this.uniq)},format:n,numInputs:null!=a?0:1}}}},sum:function(t){return null==t&&(t=A),function(e){var n;return n=e[0],function(e,r,a){return{sum:0,push:function(t){if(!isNaN(parseFloat(t[n])))return this.sum+=parseFloat(t[n])},value:function(){return this.sum},format:t,numInputs:null!=n?0:1}}}},extremes:function(t,e){return null==e&&(e=A),function(n){var r;return r=n[0],function(n,a,o){return{val:null,sorter:h(null!=n?n.sorters:void 0,r),push:function(e){var n,a,o,i;if(i=e[r],"min"!==t&&"max"!==t||(i=parseFloat(i),isNaN(i)||(this.val=Math[t](i,null!=(n=this.val)?n:i))),"first"===t&&this.sorter(i,null!=(a=this.val)?a:i)<=0&&(this.val=i),"last"===t&&this.sorter(i,null!=(o=this.val)?o:i)>=0)return this.val=i},value:function(){return this.val},format:function(t){return isNaN(t)?t:e(t)},numInputs:null!=r?0:1}}}},quantile:function(t,e){return null==e&&(e=A),function(n){var r;return r=n[0],function(n,a,o){return{vals:[],push:function(t){var e;if(e=parseFloat(t[r]),!isNaN(e))return this.vals.push(e)},value:function(){var e;return 0===this.vals.length?null:(this.vals.sort(function(t,e){return t-e}),e=(this.vals.length-1)*t,(this.vals[Math.floor(e)]+this.vals[Math.ceil(e)])/2)},format:e,numInputs:null!=r?0:1}}}},runningStat:function(t,e,n){return null==t&&(t="mean"),null==e&&(e=1),null==n&&(n=A),function(r){var a;return a=r[0],function(r,o,i){return{n:0,m:0,s:0,push:function(t){var e,n;if(n=parseFloat(t[a]),!isNaN(n))return this.n+=1,1===this.n?this.m=n:(e=this.m+(n-this.m)/this.n,this.s=this.s+(n-this.m)*(n-e),this.m=e)},value:function(){if("mean"===t)return 0===this.n?NaN:this.m;if(this.n<=e)return 0;switch(t){case"var":return this.s/(this.n-e);case"stdev":return Math.sqrt(this.s/(this.n-e))}},format:n,numInputs:null!=a?0:1}}}},sumOverSum:function(t){return null==t&&(t=A),function(e){var n,r;return r=e[0],n=e[1],function(e,a,o){return{sumNum:0,sumDenom:0,push:function(t){if(isNaN(parseFloat(t[r]))||(this.sumNum+=parseFloat(t[r])),!isNaN(parseFloat(t[n])))return this.sumDenom+=parseFloat(t[n])},value:function(){return this.sumNum/this.sumDenom},format:t,numInputs:null!=r&&null!=n?0:2}}}},sumOverSumBound80:function(t,e){return null==t&&(t=!0),null==e&&(e=A),function(n){var r,a;return a=n[0],r=n[1],function(n,o,i){return{sumNum:0,sumDenom:0,push:function(t){if(isNaN(parseFloat(t[a]))||(this.sumNum+=parseFloat(t[a])),!isNaN(parseFloat(t[r])))return this.sumDenom+=parseFloat(t[r])},value:function(){var e;return e=t?1:-1,(.821187207574908/this.sumDenom+this.sumNum/this.sumDenom+1.2815515655446004*e*Math.sqrt(.410593603787454/(this.sumDenom*this.sumDenom)+this.sumNum*(1-this.sumNum/this.sumDenom)/(this.sumDenom*this.sumDenom)))/(1+1.642374415149816/this.sumDenom)},format:e,numInputs:null!=a&&null!=r?0:2}}}},fractionOf:function(t,e,r){return null==e&&(e="total"),null==r&&(r=S),function(){var a;return a=1<=arguments.length?n.call(arguments,0):[],function(n,o,i){return{selector:{total:[[],[]],row:[o,[]],col:[[],i]}[e],inner:t.apply(null,a)(n,o,i),push:function(t){return this.inner.push(t)},format:r,value:function(){return this.inner.value()/n.getAggregator.apply(n,this.selector).inner.value()},numInputs:t.apply(null,a)().numInputs}}}}},l.countUnique=function(t){return l.uniques(function(t){return t.length},t)},l.listUnique=function(t){return l.uniques(function(e){return e.sort(f).join(t)},function(t){return t})},l.max=function(t){return l.extremes("max",t)},l.min=function(t){return l.extremes("min",t)},l.first=function(t){return l.extremes("first",t)},l.last=function(t){return l.extremes("last",t)},l.median=function(t){return l.quantile(.5,t)},l.average=function(t){return l.runningStat("mean",1,t)},l["var"]=function(t,e){return l.runningStat("var",t,e)},l.stdev=function(t,e){return l.runningStat("stdev",t,e)},s=function(t){return{Count:t.count(x),"Count Unique Values":t.countUnique(x),"List Unique Values":t.listUnique(", "),Sum:t.sum(A),"Integer Sum":t.sum(x),Average:t.average(A),Median:t.median(A),"Sample Variance":t["var"](1,A),"Sample Standard Deviation":t.stdev(1,A),Minimum:t.min(A),Maximum:t.max(A),First:t.first(A),Last:t.last(A),"Sum over Sum":t.sumOverSum(A),"80% Upper Bound":t.sumOverSumBound80(!0,A),"80% Lower Bound":t.sumOverSumBound80(!1,A),"Sum as Fraction of Total":t.fractionOf(t.sum(),"total",S),"Sum as Fraction of Rows":t.fractionOf(t.sum(),"row",S),"Sum as Fraction of Columns":t.fractionOf(t.sum(),"col",S),"Count as Fraction of Total":t.fractionOf(t.count(),"total",S),"Count as Fraction of Rows":t.fractionOf(t.count(),"row",S),"Count as Fraction of Columns":t.fractionOf(t.count(),"col",S)}}(l),b={Table:function(t,e){return g(t,e)},"Table Barchart":function(e,n){return t(g(e,n)).barchart()},Heatmap:function(e,n){return t(g(e,n)).heatmap("heatmap",n)},"Row Heatmap":function(e,n){return t(g(e,n)).heatmap("rowheatmap",n)},"Col Heatmap":function(e,n){return t(g(e,n)).heatmap("colheatmap",n)}},d={en:{aggregators:s,renderers:b,localeStrings:{renderError:"An error occurred rendering the PivotTable results.",computeError:"An error occurred computing the PivotTable results.",uiRenderError:"An error occurred rendering the PivotTable UI.",selectAll:"Select All",selectNone:"Select None",tooMany:"(too many to list)",filterResults:"Filter values",apply:"Apply",cancel:"Cancel",totals:"Totals",vs:"vs",by:"by"}}},p=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],u=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],N=function(t){return("0"+t).substr(-2,2)},c={bin:function(t,e){return function(n){return n[t]-n[t]%e}},dateFormat:function(t,e,n,r,a){var o;return null==n&&(n=!1),null==r&&(r=p),null==a&&(a=u),o=n?"UTC":"",function(n){var i;return i=new Date(Date.parse(n[t])),isNaN(i)?"":e.replace(/%(.)/g,function(t,e){switch(e){case"y":return i["get"+o+"FullYear"]();case"m":return N(i["get"+o+"Month"]()+1);case"n":return r[i["get"+o+"Month"]()];case"d":return N(i["get"+o+"Date"]());case"w":return a[i["get"+o+"Day"]()];case"x":return i["get"+o+"Day"]();case"H":return N(i["get"+o+"Hours"]());case"M":return N(i["get"+o+"Minutes"]());case"S":return N(i["get"+o+"Seconds"]());default:return"%"+e}})}}},C=/(\d+)|(\D+)/g,v=/\d/,y=/^0/,f=function(t){return function(t,e){var n,r,a,o,i,l;if(null!=e&&null==t)return-1;if(null!=t&&null==e)return 1;if("number"==typeof t&&isNaN(t))return-1;if("number"==typeof e&&isNaN(e))return 1;if(i=+t,l=+e,il)return 1;if("number"==typeof t&&"number"!=typeof e)return-1;if("number"==typeof e&&"number"!=typeof t)return 1;if("number"==typeof t&&"number"==typeof e)return 0;if(isNaN(l)&&!isNaN(i))return-1;if(isNaN(i)&&!isNaN(l))return 1;if(n=String(t),a=String(e),n===a)return 0;if(!v.test(n)||!v.test(a))return n>a?1:-1;for(n=n.match(C),a=a.match(C);n.length&&a.length;)if(r=n.shift(),o=a.shift(),r!==o)return v.test(r)&&v.test(o)?r.replace(y,".0")-o.replace(y,".0"):r>o?1:-1;return n.length-a.length}}(this),w=function(t){var e,n,r,a;r={},n={};for(e in t)a=t[e],r[a]=e,"string"==typeof a&&(n[a.toLowerCase()]=e);return function(t,e){return null!=r[t]&&null!=r[e]?r[t]-r[e]:null!=r[t]?-1:null!=r[e]?1:null!=n[t]&&null!=n[e]?n[t]-n[e]:null!=n[t]?-1:null!=n[e]?1:f(t,e)}},h=function(e,n){var r;if(null!=e)if(t.isFunction(e)){if(r=e(n),t.isFunction(r))return r}else if(null!=e[n])return e[n];return f},o=function(){function e(t,n){var a,o,i,s,u,c,h,d,p,f;null==n&&(n={}),this.getAggregator=r(this.getAggregator,this),this.getRowKeys=r(this.getRowKeys,this),this.getColKeys=r(this.getColKeys,this),this.sortKeys=r(this.sortKeys,this),this.arrSort=r(this.arrSort,this),this.input=t,this.aggregator=null!=(a=n.aggregator)?a:l.count()(),this.aggregatorName=null!=(o=n.aggregatorName)?o:"Count",this.colAttrs=null!=(i=n.cols)?i:[],this.rowAttrs=null!=(s=n.rows)?s:[],this.valAttrs=null!=(u=n.vals)?u:[],this.sorters=null!=(c=n.sorters)?c:{},this.rowOrder=null!=(h=n.rowOrder)?h:"key_a_to_z",this.colOrder=null!=(d=n.colOrder)?d:"key_a_to_z",this.derivedAttributes=null!=(p=n.derivedAttributes)?p:{},this.filter=null!=(f=n.filter)?f:function(){return!0},this.tree={},this.rowKeys=[],this.colKeys=[],this.rowTotals={},this.colTotals={},this.allTotal=this.aggregator(this,[],[]),this.sorted=!1,e.forEachRecord(this.input,this.derivedAttributes,function(t){return function(e){if(t.filter(e))return t.processRecord(e)}}(this))}return e.forEachRecord=function(e,n,r){var o,i,l,s,u,c,h,d,p,f,m,g;if(o=t.isEmptyObject(n)?r:function(t){var e,a,o;for(e in n)o=n[e],t[e]=null!=(a=o(t))?a:t[e];return r(t)},t.isFunction(e))return e(o);if(t.isArray(e)){if(t.isArray(e[0])){f=[];for(l in e)if(a.call(e,l)&&(i=e[l],l>0)){d={},p=e[0];for(s in p)a.call(p,s)&&(u=p[s],d[u]=i[s]);f.push(o(d))}return f}for(m=[],c=0,h=e.length;c tr > th",e).each(function(e){return g.push(t(this).text())}),t("tbody > tr",e).each(function(e){return d={},t("td",this).each(function(e){return d[g[e]]=t(this).text()}),o(d)});throw new Error("unknown input format")},e.prototype.forEachMatchingRecord=function(t,n){return e.forEachRecord(this.input,this.derivedAttributes,function(e){return function(r){var a,o,i;if(e.filter(r)){for(a in t)if(i=t[a],i!==(null!=(o=r[a])?o:"null"))return;return n(r)}}}(this))},e.prototype.arrSort=function(t){var e,n;return n=function(){var n,r,a;for(a=[],n=0,r=t.length;n=l;c=0<=l?++r:--r)t[e-1][c]!==t[e][c]&&(i=!1);if(i)return-1}for(a=0;e+a=s;c=0<=s?++o:--o)t[e][c]!==t[e+a][c]&&(u=!0);if(u)break;a++}return a},A=document.createElement("thead");for(d in i)if(a.call(i,d)){o=i[d],S=document.createElement("tr"),0===parseInt(d)&&0!==m.length&&(w=document.createElement("th"),w.setAttribute("colspan",m.length),w.setAttribute("rowspan",i.length),S.appendChild(w)),w=document.createElement("th"),w.className="pvtAxisLabel",w.textContent=o,S.appendChild(w);for(h in s)a.call(s,h)&&(l=s[h],k=b(s,parseInt(h),parseInt(d)),k!==-1&&(w=document.createElement("th"),w.className="pvtColLabel",w.textContent=l[d],w.setAttribute("colspan",k),parseInt(d)===i.length-1&&0!==m.length&&w.setAttribute("rowspan",2),S.appendChild(w)));0===parseInt(d)&&n.table.rowTotals&&(w=document.createElement("th"),w.className="pvtTotalLabel pvtRowTotalLabel",w.innerHTML=n.localeStrings.totals,w.setAttribute("rowspan",i.length+(0===m.length?0:1)),S.appendChild(w)),A.appendChild(S)}if(0!==m.length){S=document.createElement("tr");for(h in m)a.call(m,h)&&(p=m[h],w=document.createElement("th"),w.className="pvtAxisLabel",w.textContent=p,S.appendChild(w));w=document.createElement("th"),0===i.length&&(w.className="pvtTotalLabel pvtRowTotalLabel",w.innerHTML=n.localeStrings.totals),S.appendChild(w),A.appendChild(S)}f.appendChild(A),C=document.createElement("tbody");for(h in v)if(a.call(v,h)){g=v[h],S=document.createElement("tr");for(d in g)a.call(g,d)&&(N=g[d],k=b(v,parseInt(h),parseInt(d)),k!==-1&&(w=document.createElement("th"),w.className="pvtRowLabel",w.textContent=N,w.setAttribute("rowspan",k),parseInt(d)===m.length-1&&0!==i.length&&w.setAttribute("colspan",2),S.appendChild(w)));for(d in s)a.call(s,d)&&(l=s[d],r=e.getAggregator(g,l),T=r.value(),y=document.createElement("td"),y.className="pvtVal row"+h+" col"+d,y.textContent=r.format(T),y.setAttribute("data-value",T),null!=c&&(y.onclick=c(T,g,l)),S.appendChild(y));(n.table.rowTotals||0===i.length)&&(x=e.getAggregator(g,[]),T=x.value(),y=document.createElement("td"),y.className="pvtTotal rowTotal",y.textContent=x.format(T),y.setAttribute("data-value",T),null!=c&&(y.onclick=c(T,g,[])),y.setAttribute("data-for","row"+h),S.appendChild(y)),C.appendChild(S)}if(n.table.colTotals||0===m.length){S=document.createElement("tr"),(n.table.colTotals||0===m.length)&&(w=document.createElement("th"),w.className="pvtTotalLabel pvtColTotalLabel",w.innerHTML=n.localeStrings.totals,w.setAttribute("colspan",m.length+(0===i.length?0:1)),S.appendChild(w));for(d in s)a.call(s,d)&&(l=s[d],x=e.getAggregator([],l),T=x.value(),y=document.createElement("td"),y.className="pvtTotal colTotal",y.textContent=x.format(T),y.setAttribute("data-value",T),null!=c&&(y.onclick=c(T,[],l)),y.setAttribute("data-for","col"+d),S.appendChild(y));(n.table.rowTotals||0===i.length)&&(x=e.getAggregator([],[]),T=x.value(),y=document.createElement("td"),y.className="pvtGrandTotal",y.textContent=x.format(T),y.setAttribute("data-value",T),null!=c&&(y.onclick=c(T,[],[])),S.appendChild(y)),C.appendChild(S)}return f.appendChild(C),f.setAttribute("data-numrows",v.length),f.setAttribute("data-numcols",s.length),f},t.fn.pivot=function(e,n,r){var a,i,s,u,c,h,p,f;null==r&&(r="en"),null==d[r]&&(r="en"),a={cols:[],rows:[],vals:[],rowOrder:"key_a_to_z",colOrder:"key_a_to_z",dataClass:o,filter:function(){return!0},aggregator:l.count()(),aggregatorName:"Count",sorters:{},derivedAttributes:{},renderer:g},u=t.extend(!0,{},d.en.localeStrings,d[r].localeStrings),s={rendererOptions:{localeStrings:u},localeStrings:u},c=t.extend(!0,{},s,t.extend({},a,n)),p=null;try{h=new c.dataClass(e,c);try{p=c.renderer(h,c.rendererOptions)}catch(m){i=m,"undefined"!=typeof console&&null!==console&&console.error(i.stack),p=t("").html(c.localeStrings.renderError)}}catch(m){i=m,"undefined"!=typeof console&&null!==console&&console.error(i.stack),p=t("").html(c.localeStrings.computeError)}for(f=this[0];f.hasChildNodes();)f.removeChild(f.lastChild);return this.append(p)},t.fn.pivotUI=function(n,r,i,l){var s,u,c,p,m,g,v,b,C,y,w,A,x,S,N,T,k,O,_,F,D,E,M,R,I,L,U,K,q,z,V,j,H,B,P,J,G,W,$,Q,Y,X,Z,tt,et;null==i&&(i=!1),null==l&&(l="en"),null==d[l]&&(l="en"),b={derivedAttributes:{},aggregators:d[l].aggregators,renderers:d[l].renderers,hiddenAttributes:[],hiddenFromAggregators:[],hiddenFromDragDrop:[],menuLimit:500,cols:[],rows:[],vals:[],rowOrder:"key_a_to_z",colOrder:"key_a_to_z",dataClass:o,exclusions:{},inclusions:{},unusedAttrsVertical:85,autoSortUnusedAttrs:!1,onRefresh:null,showUI:!0,filter:function(){return!0},sorters:{}},_=t.extend(!0,{},d.en.localeStrings,d[l].localeStrings),O={rendererOptions:{localeStrings:_},localeStrings:_},y=this.data("pivotUIOptions"),M=null==y||i?t.extend(!0,{},O,t.extend({},b,r)):y;try{m={},F=[],L=0,o.forEachRecord(n,M.derivedAttributes,function(t){var e,n,r,o;if(M.filter(t)){F.push(t);for(e in t)a.call(t,e)&&null==m[e]&&(m[e]={},L>0&&(m[e]["null"]=L));for(e in m)o=null!=(r=t[e])?r:"null",null==(n=m[e])[o]&&(n[o]=0),m[e][o]++;return L++}}),Y=t("",{"class":"pvtUi"}).attr("cellpadding",5),B=t("").appendTo(Y),u=t("").appendTo(Y),Q.append(t("").append(B).append(X)),this.html(Y),q=M.cols,D=0,T=q.length;Dp;et=0<=p?++g:--g){for(i=t("
").addClass("pvtUiCell"),H=t("").addClass("pvtAxisContainer pvtUnused pvtUiCell"),J=function(){var t;t=[];for(s in m)e.call(M.hiddenAttributes,s)<0&&t.push(s);return t}(),G=function(){var t,n,r;for(r=[],t=0,n=J.length;tZ}M.unusedAttrsVertical===!0||tt?X.addClass("pvtVertList"):X.addClass("pvtHorizList"),w=function(n){var r,a,o,i,l,s,u,c,d,p,f,g,v,b,C,y,w,x,S;if(S=function(){var t;t=[];for(C in m[n])t.push(C);return t}(),c=!1,x=t("
").addClass("pvtFilterBox").hide(),x.append(t("

").append(t("").text(n),t("").addClass("count").text("("+S.length+")"))),S.length>M.menuLimit)x.append(t("

").html(M.localeStrings.tooMany));else for(S.length>5&&(i=t("

").appendTo(x),v=h(M.sorters,n),f=M.localeStrings.filterResults,t("",{type:"text"}).appendTo(i).attr({placeholder:f,"class":"pvtSearch"}).bind("keyup",function(){var n,r,a;return a=t(this).val().toLowerCase().trim(),r=function(t,n){return function(r){var o,i;return o=a.substring(t.length).trim(),0===o.length||(i=Math.sign(v(r.toLowerCase(),o)),e.call(n,i)>=0)}},n=0===a.indexOf(">=")?r(">=",[1,0]):0===a.indexOf("<=")?r("<=",[-1,0]):0===a.indexOf(">")?r(">",[1]):0===a.indexOf("<")?r("<",[-1]):0===a.indexOf("~")?function(t){return 0===a.substring(1).trim().length||t.toLowerCase().match(a.substring(1))}:function(t){return t.toLowerCase().indexOf(a)!==-1},x.find(".pvtCheckContainer p label span.value").each(function(){return n(t(this).text())?t(this).parent().parent().show():t(this).parent().parent().hide()})}),i.append(t("
")),t("

").addClass("pvtVals pvtUiCell").appendTo($).append(u).append(P).append(v).append(t("
")),t("
").addClass("pvtAxisContainer pvtHorizList pvtCols pvtUiCell").appendTo($),Q=t("
").addClass("pvtAxisContainer pvtRows pvtUiCell").attr("valign","top")),I=t("").attr("valign","top").addClass("pvtRendererArea").appendTo(Q),M.unusedAttrsVertical===!0||tt?(Y.find("tr:nth-child(1)").prepend(B),Y.find("tr:nth-child(2)").prepend(X)):Y.prepend(t("