[ADD] REPORT: Add reporting module for Horilla (#750)

This commit is contained in:
Horilla
2025-05-19 16:51:04 +05:30
committed by GitHub
parent caf8e5a3d4
commit 0b327c4923
35 changed files with 5073 additions and 0 deletions

0
report/__init__.py Normal file
View File

3
report/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

18
report/apps.py Normal file
View File

@@ -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

View File

3
report/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

90
report/sidebar.py Normal file
View File

@@ -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")

View File

@@ -0,0 +1,381 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
{% load widget_tweaks %}
{% load assets_custom_filter %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Asset Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Asset" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_name.id_for_label}}">{% trans "Asset Name" %}</label>
{{asset_filter_form.asset_name}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_tracking_id.id_for_label}}">{% trans "Tracking Id" %}</label>
{{asset_filter_form.asset_tracking_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_purchase_date.id_for_label}}">{% trans "Purchase Date" %}</label>
{{asset_filter_form.asset_purchase_date |attr:"type:date"}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_purchase_cost.id_for_label}}">{% trans "Purchase Cost" %}</label>
{{asset_filter_form.asset_purchase_cost}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_lot_number_id.id_for_label}}">{% trans "Asset Batch Number" %}</label>
{{asset_filter_form.asset_lot_number_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_category_id.id_for_label}}">{% trans "Category" %}</label>
{{asset_filter_form.asset_category_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="oh-input-group">
<label class="oh-label" for="{{asset_filter_form.asset_status.id_for_label}}">{% trans "Status" %}</label>
{{asset_filter_form.asset_status}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
</div>
<script>
// Function to load filtered pivot data
function loadFilteredPivotData() {
// Get filter form data
var formData = $("#filterForm").serialize(); // Serializing the form
$.ajax({
url: "asset-pivot", // Replace with your URL to fetch filtered data
method: "GET",
data: formData,
success: function (data) {
// Render the pivot table with the filtered data
$("#pivot-container").pivotUI(data, {
rows: ["Asset Name","Category","Tracking ID","Batch Number","Status","Asset Cost","Asset User","Phone"],
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
},
renderers: $.extend(
$.pivotUtilities.renderers,
$.pivotUtilities.plotly_renderers // Adding Plotly renderers
)
});
},
error: function (error) {
console.log("Error fetching filtered data: ", error);
}
});
}
$(document).ready(function () {
// Initialize the pivot table on page load
$.getJSON("asset-pivot", function (data) {
$("#pivot-container").pivotUI(data, {
rows: ["Asset Name","Category","Tracking ID","Batch Number","Status","Asset Cost","Asset User","Phone"],
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
},
renderers: $.extend(
$.pivotUtilities.renderers,
$.pivotUtilities.plotly_renderers // Adding Plotly renderers
)
});
});
// When the filter form is submitted, prevent default action and load filtered data
$("#filterForm").submit(function (event) {
event.preventDefault();
loadFilteredPivotData(); // Call the function to load filtered data
});
// Export to Excel on button click
$("#export-btn").on("click", function () {
exportTableToExcel("pivot-container", "pivot_report.xlsx");
});
});
// Export FunctionN
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3;
// Table rendering
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
</script>
{% endblock content %}

View File

@@ -0,0 +1,524 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
<div class="oh-wrapper mt-3">
{% include 'filter_tags.html' %}
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Attendance Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Attendance" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Employee" %}</label>
{{f.form.employee_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Department" %}</label>
{{f.form.employee_id__employee_work_info__department_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Shift" %}</label>
{{f.form.shift_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Company" %}</label>
{{f.form.employee_id__employee_work_info__company_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Job Position" %}</label>
{{f.form.employee_id__employee_work_info__job_position_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Work Type" %}</label>
{{f.form.work_type_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Attendance Date" %}</label>
{{f.form.attendance_date}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "In Time" %}</label>
{{f.form.attendance_clock_in}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Min Hour" %}</label>
{{f.form.minimum_hour}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Batch" %}</label>
{{f.form.batch_attendance_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Out Time" %}</label>
{{f.form.attendance_clock_out}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Attendance From" %}</label>
{{f.form.attendance_date__gte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "In From" %}</label>
{{f.form.attendance_clock_in__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Out From" %}</label>
{{f.form.attendance_clock_out__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "At Work Greater or Equal" %}</label>
{{f.form.at_work_second__gte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "OT Greater or Equal" %}</label>
{{f.form.overtime_second__gte}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Attendance Till" %}</label>
{{f.form.attendance_date__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "In Till" %}</label>
{{f.form.attendance_clock_in__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Out Till" %}</label>
{{f.form.attendance_clock_out__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "At Work Less Than or Equal" %}</label>
{{f.form.at_work_second__lte}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "OT Less Than or Equal" %}</label>
{{f.form.overtime_second__lte}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
</div>
<script>
// Function to load filtered pivot data
function loadFilteredPivotData() {
// Get filter form data
var formData = $("#filterForm").serialize(); // Serializing the form
$.ajax({
url: "attendance-pivot", // Replace with your URL to fetch filtered data
method: "GET",
data: formData,
success: function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#pivot-container").pivotUI(data, {
rows: ["Name","Phone","Department","Shift","Attendance Date","Attendance Day","Worked Hour"], // Default rows
cols: [], // Default columns
aggregatorName: "Count", // Default aggregator
rendererName: "Table", // Default view as Table
// Hide decimal fields from rows and columns but keep them available for aggregation
hiddenFromDragDrop: [
"Clock-in Decimal",
"Clock-out Decimal",
"At Work Decimal",
"Worked Hour Decimal",
"Minimum Hour Decimal",
"Overtime Decimal"
],
unusedAttrsVertical: true, // Keep available in the left-side panel for aggregation
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
),
// Hide specific fields from selection dropdown
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (
currentRenderer === "Table" ||
currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" ||
currentRenderer === "Row Heatmap" ||
currentRenderer === "Col Heatmap"
) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
// Hide fields from dropdown but keep them available in the table
let hiddenFields = [
"Clock-in",
"Clock-out",
"Worked Hour",
"Minimum Hour",
"Overtime",
"At Work"
];
setTimeout(function () {
$(".pvtAttrDropdown option").each(function () {
if (hiddenFields.includes($(this).text())) {
$(this).remove(); // Remove from selection
}
});
}, 10);
}
});
},
error: function (error) {
console.log("Error fetching filtered data: ", error);
}
});
}
$(document).ready(function () {
// Initialize the pivot table on page load
$.getJSON("attendance-pivot", function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#pivot-container").pivotUI(data, {
rows: ["Name","Phone","Department","Shift","Attendance Date","Attendance Day","Worked Hour"], // Default rows
cols: [], // Default columns
aggregatorName: "Count", // Default aggregator
rendererName: "Table", // Default view as Table
// Hide decimal fields from rows and columns but keep them available for aggregation
hiddenFromDragDrop: [
"Clock-in Decimal",
"Clock-out Decimal",
"At Work Decimal",
"Worked Hour Decimal",
"Minimum Hour Decimal",
"Overtime Decimal"
],
unusedAttrsVertical: true, // Keep available in the left-side panel for aggregation
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
),
// Hide specific fields from selection dropdown
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (
currentRenderer === "Table" ||
currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" ||
currentRenderer === "Row Heatmap" ||
currentRenderer === "Col Heatmap"
) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
// Hide fields from dropdown but keep them available in the table
let hiddenFields = [
"Clock-in",
"Clock-out",
"Worked Hour",
"Minimum Hour",
"Overtime",
"At Work"
];
setTimeout(function () {
$(".pvtAttrDropdown option").each(function () {
if (hiddenFields.includes($(this).text())) {
$(this).remove(); // Remove from selection
}
});
}, 10);
}
});
});
// When the filter form is submitted, prevent default action and load filtered data
$("#filterForm").submit(function (event) {
event.preventDefault();
loadFilteredPivotData(); // Call the function to load filtered data
});
// Export to Excel on button click
$("#export-btn").on("click", function () {
exportTableToExcel("pivot-container", "pivot_report.xlsx");
});
});
// Export FunctionN
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3;
// Table rendering
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
</script>
{% endblock content %}

View File

@@ -0,0 +1,411 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Employee Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Employee" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_first_name.id_for_label}}">{% trans "First Name" %}</label>
{{f.form.employee_first_name}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.email.id_for_label}}">{% trans "Email" %}</label>
{{f.form.email}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.country.id_for_label}}">{% trans "Country" %}</label>
{{f.form.country}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_last_name.id_for_label}}">{% trans "Last Name" %}</label>
{{f.form.employee_last_name}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.phone.id_for_label}}">{% trans "Phone" %}</label>
{{f.form.phone}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.gender.id_for_label}}">{% trans "Gender" %}</label>
{{f.form.gender}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__company_id.id_for_label}}">{% trans "Company" %}</label>
{{f.form.employee_work_info__company_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__department_id.id_for_label}}">{% trans "Department" %}</label>
{{f.form.employee_work_info__department_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__job_role_id.id_for_label}}">{% trans "Job Role" %}</label>
{{f.form.employee_work_info__job_role_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__shift_id.id_for_label}}">{% trans "Shift" %}</label>
{{f.form.employee_work_info__shift_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__reporting_manager_id.id_for_label}}">{% trans "Reporting Manager" %}</label>
{{f.form.employee_work_info__reporting_manager_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__job_position_id.id_for_label}}">{% trans "Job Position" %}</label>
{{f.form.employee_work_info__job_position_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__work_type_id.id_for_label}}">{% trans "Work Type" %}</label>
{{f.form.employee_work_info__work_type_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_work_info__employee_type_id.id_for_label}}">{% trans "Employee Type" %}</label>
{{f.form.employee_work_info__employee_type_id}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
</div>
<script>
// Function to load filtered pivot data
function loadFilteredPivotData() {
// Get filter form data
var formData = $("#filterForm").serialize(); // Serializing the form
$.ajax({
url: "employee-pivot", // Replace with your URL to fetch filtered data
method: "GET",
data: formData,
success: function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#pivot-container").pivotUI(data, {
rows: ["Department","Job Position","Job Role","Name","Email","Phone"],
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
},
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
)
});
},
error: function (error) {
console.log("Error fetching filtered data: ", error);
}
});
}
$(document).ready(function () {
// Initialize the pivot table on page load
$.getJSON("employee-pivot", function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#pivot-container").pivotUI(data, {
rows: ["Department","Job Position","Job Role","Name","Email","Phone"],
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
},
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
)
});
});
// When the filter form is submitted, prevent default action and load filtered data
$("#filterForm").submit(function (event) {
event.preventDefault();
loadFilteredPivotData(); // Call the function to load filtered data
});
// Export to Excel on button click
$("#export-btn").on("click", function () {
exportTableToExcel("pivot-container", "pivot_report.xlsx");
});
});
// Export FunctionN
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3;
// Table rendering
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
</script>
{% endblock content %}

View File

@@ -0,0 +1,587 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
{% include 'filter_tags.html' %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Leave Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Leave Request" %}</div>
<div class="oh-accordion-body">
<input type="hidden" value="{{request.GET.id}}" name="id">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id.id_for_label }}">{% trans "Employees" %}</label>
{{form.employee_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.leave_type_id.id_for_label }}">{% trans "Leave Type" %}</label>
{{form.leave_type_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.start_date.id_for_label }}">{% trans "Start Date" %}</label>
{{form.start_date}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.end_date.id_for_label }}">{% trans "End Date" %}</label>
{{form.end_date}}
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.status.id_for_label }}">{% trans "Status" %}</label>
{{form.status}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.from_date.id_for_label }}">{% trans "From Date" %}</label>
{{form.from_date}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.to_date.id_for_label }}">{% trans "To Date" %}</label>
{{form.to_date}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__company_id.id_for_label }}">{% trans "Company" %}</label>
{{form.employee_id__employee_work_info__company_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__reporting_manager_id.id_for_label }}">{% trans "Reporting Manager" %}</label>
{{form.employee_id__employee_work_info__reporting_manager_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__department_id.id_for_label }}">{% trans "Department" %}</label>
{{form.employee_id__employee_work_info__department_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__job_position_id.id_for_label }}">{% trans "Job Position" %}</label>
{{form.employee_id__employee_work_info__job_position_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__shift_id.id_for_label }}">{% trans "Shift" %}</label>
{{form.employee_id__employee_work_info__shift_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{ form.employee_id__employee_work_info__work_type_id.id_for_label }}">{% trans "Work Type" %}</label>
{{form.employee_id__employee_work_info__work_type_id}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Available Leave" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Employees" %}</label>
{{f.form.employee_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Leave Type" %}</label>
{{f.form.leave_type_id}}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Available Days" %}</label>
{{f.form.available_days}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Carryforward Days" %}</label>
{{f.form.carryforward_days}}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Total Leave Days" %}</label>
{{f.form.total_leave_days}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Assigned Date" %}</label>
{{f.form.assigned_date}}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Available Days Greater or Equal" %}</label
>
{{f.form.available_days__gte}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Available Days Less Than or Equal" %}</label
>
{{f.form.available_days__lte}}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Carryforward Days Greater or Equal" %}</label
>
{{f.form.carryforward_days__gte}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Carryforward Days Less Than or Equal" %}</label
>
{{f.form.carryforward_days__lte}}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Total Leave Days Greater or Equal" %}</label
>
{{f.form.total_leave_days__gte}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label"
>{% trans "Total Leave Days Less Than or Equal" %}</label
>
{{f.form.total_leave_days__lte}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Model Selection Dropdown -->
<span class="ms-1 fw-bold" style="font-size:16px;">Choose Report : </span>
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
<option value="leave_request">Leave Request</option>
<option value="available_leave">Available Leave</option>
</select>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
<div id="pivot-leave" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-availableleave" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
</div>
</div>
<script>
$(function () {
// Function to load pivot data dynamically
function loadPivotData(model) {
let url = `leave-pivot?model=${model}`;
// Hide all container
$(".pivot-wrapper").hide();
let containerId = "";
let rowsConfig = [];
if (model === "leave_request"){
containerId = "pivot-leave";
rowsConfig = ["Name","Department","Shift", "Leave Type","Requested Days","Start Date","End Date","Status"]
}else if (model === "available_leave"){
containerId = "pivot-availableleave";
rowsConfig = ["Name","Department", "Leave Type","Assigned Date","Total Leave Days","Available Days","Carryforward Days"]
}
// Show relevant container
$("#" + containerId).show();
// Fetch data dynamically based on model
$.getJSON(url, function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (
currentRenderer === "Table" ||
currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" ||
currentRenderer === "Row Heatmap" ||
currentRenderer === "Col Heatmap"
) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
},
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers), // Add Plotly renderers
});
});
}
window.loadFilteredPivotData =function loadFilteredPivotData() {
const selectedModel = $("#model-select").val();
const formData = $("#filterForm").serialize();
$(".pivot-wrapper").hide();
let containerId = "";
let rowsConfig = [];
if (selectedModel === "leave_request"){
containerId = "pivot-leave";
rowsConfig = ["Name","Department","Shift", "Leave Type","Requested Days","Start Date","End Date","Status"]
}else if (selectedModel === "available_leave"){
containerId = "pivot-availableleave";
rowsConfig = ["Name","Department", "Leave Type","Assigned Date","Total Leave Days","Available Days","Carryforward Days"]
}
$("#" + containerId).show();
$.getJSON(`leave-pivot?model=${selectedModel}&${formData}`, function (data) {
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table",
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
onRefresh: function (config) {
const currentRenderer = config.rendererName;
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
$("#export-btn").show();
} else {
$("#export-btn").hide();
}
}
});
});
}
// Initial load with all models
loadPivotData("leave_request");
// Model selection change event
$("#model-select").on("change", function () {
let selectedModel = $(this).val();
loadPivotData(selectedModel); // Reload pivot data with selected model
});
// Export to Excel on button click
$("#export-btn").on("click", function () {
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
if (visiblePivot.length) {
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
}
});
// Export Function
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3; // Leave some rows before the table
// ------------------------
// Render pivot table
// ------------------------
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
// Merge
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
// Auto-adjust column widths
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
// Save
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,583 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Payroll Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Payslip" %}</div>
<div class="oh-accordion-body">
<div class="row">
{% if perms.payroll.view_payslip %}
<div class="col-sm-12">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.employee_id.id_for_label}}">{% trans "Employee" %}</label>
{{ f.form.employee_id }}
</div>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.status.id_for_label}}">{% trans "Status" %}</label>
{{ f.form.status }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.group_name.id_for_label}}">{% trans "Batch" %}</label>
{{ f.form.group_name }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.start_date_from.id_for_label}}">{% trans "Start Date From" %}</label>
{{ f.form.start_date_from }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.start_date_till.id_for_label}}">{% trans "Start Date Till" %}</label>
{{ f.form.start_date_till }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.end_date_from.id_for_label}}">{% trans "End Date From" %}</label>
{{ f.form.end_date_from }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.end_date_till.id_for_label}}">{% trans "End Date Till" %}</label>
{{ f.form.end_date_till }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.gross_pay__lte.id_for_label}}"
>{% trans "Gross Pay Less Than or Equal" %}</label
>
{{ f.form.gross_pay__lte }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.gross_pay__gte.id_for_label}}"
>{% trans "Gross Pay Greater or Equal" %}</label
>
{{ f.form.gross_pay__gte }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.deduction__lte.id_for_label}}"
>{% trans "Deduction Less Than or Equal" %}</label
>
{{ f.form.deduction__lte }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.deduction__gte.id_for_label}}"
>{% trans "Deduction Greater or Equal" %}</label
>
{{ f.form.deduction__gte }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.net_pay__lte.id_for_label}}"
>{% trans "Net Pay Less Than or Equal" %}</label
>
{{ f.form.net_pay__lte }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.net_pay__gte.id_for_label}}"
>{% trans "Net Pay Greater or Equal" %}</label
>
{{ f.form.net_pay__gte }}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Allowance & Deduction" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.allowance_amount_gte.id_for_label}}">{% trans "Allowance Greater than" %}</label>
{{ f.form.allowance_amount_gte }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.allowance_amount_lte.id_for_label}}">{% trans "Allowance Less than" %}</label>
{{ f.form.allowance_amount_lte }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.deduction_amount_gte.id_for_label}}">{% trans "Deduction Greater than" %}</label>
{{ f.form.deduction_amount_gte }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.deduction_amount_lte.id_for_label}}">{% trans "Deduction Less than" %}</label>
{{ f.form.deduction_amount_lte }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.start_date_from.id_for_label}}">{% trans "Start Date From" %}</label>
{{ f.form.start_date_from }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.start_date_till.id_for_label}}">{% trans "Start Date Till" %}</label>
{{ f.form.start_date_till }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.end_date_from.id_for_label}}">{% trans "End Date From" %}</label>
{{ f.form.end_date_from }}
</div>
</div>
<div class="col-sm-6">
<div class="oh-input-group">
<label class="oh-label" for="{{f.form.end_date_till.id_for_label}}">{% trans "End Date Till" %}</label>
{{ f.form.end_date_till }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Model Selection Dropdown -->
<span class="ms-1 fw-bold" style="font-size:16px;">Choose Report : </span>
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
<option value="payslip">Payslip</option>
<option value="allowance">Allowance & Deduction</option>
</select>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
<div id="pivot-payslip" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-allowance" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
</div>
</div>
<script>
$(function () {
// Function to load pivot data dynamically
function loadPivotData(model) {
let url = `payroll-pivot?model=${model}`;
// Hide all containers first
$(".pivot-wrapper").hide();
// Determine current container and row config
let containerId = "";
let rowsConfig = [];
if (model === "payslip") {
containerId = "pivot-payslip";
rowsConfig = ["Employee","Basic Salary","Gross Pay","Net Pay","Status"];
} else if (model === "allowance") {
containerId = "pivot-allowance";
rowsConfig = ["Employee","Allowance & Deduction","Allowance & Deduction Title","Allowance & Deduction Amount"];
}
// Show relevant container
$("#" + containerId).show();
$.getJSON(url, function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [], // Default columns
aggregatorName: "Count", // Default aggregator
rendererName: "Table", // Default view as Table
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
),
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (
currentRenderer === "Table" ||
currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" ||
currentRenderer === "Row Heatmap" ||
currentRenderer === "Col Heatmap"
) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
// ✅ Hide fields from the dropdown but keep them visible in the table
let hiddenFields = ["Allowance Amount", "Deduction Amount"];
setTimeout(function () {
$(".pvtAttrDropdown option").each(function () {
if (hiddenFields.includes($(this).text())) {
$(this).remove(); // Remove from selection
}
});
}, 10);
},
});
window.loadFilteredPivotData =function loadFilteredPivotData() {
const selectedModel = $("#model-select").val();
const formData = $("#filterForm").serialize();
$(".pivot-wrapper").hide();
let containerId = "";
let rowsConfig = [];
if (model === "payslip") {
containerId = "pivot-payslip";
rowsConfig = ["Employee","Basic Salary","Gross Pay","Net Pay","Status"];
} else if (model === "allowance") {
containerId = "pivot-allowance";
rowsConfig = ["Employee","Allowance & Deduction","Allowance & Deduction Title","Allowance & Deduction Amount"];
}
$("#" + containerId).show();
$.getJSON(`payroll-pivot?model=${selectedModel}&${formData}`, function (data) {
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table",
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
onRefresh: function (config) {
const currentRenderer = config.rendererName;
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
$("#export-btn").show();
} else {
$("#export-btn").hide();
}
}
});
});
}
// Export to Excel on button click
$("#export-btn").on("click", function () {
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
if (visiblePivot.length) {
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
}
});
// Export Function
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3; // Leave some rows before the table
// ------------------------
// Render pivot table
// ------------------------
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
// Merge
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
// Auto-adjust column widths
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
// Save
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
});
}
// Initial load with all models
loadPivotData("payslip");
// Model selection change event
$("#model-select").on("change", function () {
let selectedModel = $(this).val();
loadPivotData(selectedModel); // Reload pivot data with selected model
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,526 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Performance Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Objective" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for=""
>{% trans "Managers" %}</label
>
{{objective_filer_form.managers}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Key Result" %}</label>
{{objective_filer_form.employee_objective__key_result_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Assignees" %}</label>
{{objective_filer_form.assignees}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Duration" %} </label>
{{objective_filer_form.duration}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Employee Objective" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for=""
>{% trans "Employees" %}</label
>
{{emp_obj_form.form.employee_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Start Date From" %}</label>
{{emp_obj_form.form.start_date_from}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Key Result" %}</label>
{{emp_obj_form.form.key_result_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Start Date Till" %} </label>
{{emp_obj_form.form.start_date_till}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for=""
>{% trans "End Date From" %}</label
>
{{emp_obj_form.form.end_date_from}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "Status" %}</label>
{{emp_obj_form.form.status}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="">{% trans "End Date Till" %}</label>
{{emp_obj_form.form.end_date_till}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Feedback" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.review_cycle.id_for_label}}">{% trans "Feedback Title" %}</label>
{{feedback_filter_form.review_cycle}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.status.id_for_label}}">{% trans "Status" %} </label>
{{feedback_filter_form.status}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.employee_id.id_for_label}}">{% trans "Employee" %}</label>
{{feedback_filter_form.employee_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.manager_id.id_for_label}}">{% trans "Manager" %} </label>
{{feedback_filter_form.manager_id}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.colleague_id.id_for_label}}">{% trans "Colleague" %} </label>
{{feedback_filter_form.colleague_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.start_date.id_for_label}}">{% trans "Start Date" %}</label>
{{feedback_filter_form.start_date}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.subordinate_id.id_for_label}}">{% trans "Subordinate" %}</label>
{{feedback_filter_form.subordinate_id}}
</div>
<div class="oh-input-group">
<label class="oh-label" for="{{feedback_filter_form.end_date.id_for_label}}">{% trans "End Date" %}</label>
{{feedback_filter_form.end_date}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Model Selection Dropdown -->
<span class="ms-1 fw-bold" style="font-size:16px;">Choose Report : </span>
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
<option value="objective">Objectives</option>
<option value="employeeobjective">Employee Objective</option>
<option value="feedback">Feedback</option>
</select>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
<div id="pivot-feedback" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-objective" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-employeeobjective" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
</div>
</div>
<script>
$(function () {
function loadPivotData(model) {
// Hide all containers first
$(".pivot-wrapper").hide();
// Determine current container and row config
let containerId = "";
let rowsConfig = [];
if (model === "feedback") {
containerId = "pivot-feedback";
rowsConfig = ["Title","Employee","Manager","Answerable Employees","Answered Employees","Questions","Answer"];
} else if (model === "objective") {
containerId = "pivot-objective";
rowsConfig = ["Objective","Key Results","Manager","Assignees"];
} else if (model === "employeeobjective") {
containerId = "pivot-employeeobjective";
rowsConfig = ["Employee", "Objective","Employee Keyresult","Keyresult Target Value","Keyresult Current Value"];
}
// Show relevant container
$("#" + containerId).show();
// Fetch and render data in its own container
$.getJSON(`pms-pivot?model=${model}`, function (data) {
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table",
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
onRefresh: function (config) {
const currentRenderer = config.rendererName;
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
$("#export-btn").show();
} else {
$("#export-btn").hide();
}
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
setTimeout(() => {
$(".pvtAttrDropdown option").each(function () {
if (hiddenFields.includes($(this).text())) {
$(this).remove();
}
});
}, 10);
}
});
});
}
window.loadFilteredPivotData =function loadFilteredPivotData() {
const selectedModel = $("#model-select").val();
const formData = $("#filterForm").serialize();
$(".pivot-wrapper").hide();
let containerId = "";
let rowsConfig = [];
if (selectedModel === "feedback") {
containerId = "pivot-feedback";
rowsConfig = ["Title","Employee", "Manager", "Answerable Employees", "Answered Employees", "Questions", "Answer"];
} else if (selectedModel === "objective") {
containerId = "pivot-objective";
rowsConfig = ["Objective", "Key Results", "Manager", "Assignees"];
} else if (selectedModel === "employeeobjective") {
containerId = "pivot-employeeobjective";
rowsConfig = ["Employee", "Objective", "Employee Keyresult", "Keyresult Target Value", "Keyresult Current Value"];
}
$("#" + containerId).show();
$.getJSON(`pms-pivot?model=${selectedModel}&${formData}`, function (data) {
console.log("Filtered data:", data);
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table",
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
onRefresh: function (config) {
const currentRenderer = config.rendererName;
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
$("#export-btn").show();
} else {
$("#export-btn").hide();
}
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
}
});
});
}
// Listen to dropdown
$("#model-select").on("change", function () {
let selectedModel = $(this).val();
loadPivotData(selectedModel);
});
// Initial load
loadPivotData("objective");
// Export to Excel on button click
$("#export-btn").on("click", function () {
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
if (visiblePivot.length) {
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
}
});
// Export Function
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3; // Leave some rows before the table
// ------------------------
// Render pivot table
// ------------------------
const cellMap = {};
Array.from(table.rows).forEach((row, rowIndex) => {
if (
row.classList.contains("pvtTotal") ||
row.classList.contains("pvtGrandTotal")
) return;
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
if (
cell.classList.contains("pvtTotal") ||
cell.classList.contains("pvtTotalLabel") ||
cell.classList.contains("pvtAggregator") ||
cell.classList.contains("pvtGrandTotal")
) return;
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
excelCell.font = {
bold: rowIndex === 0,
size: rowIndex === 0 ? 12 : 11,
color: { argb: rowIndex === 0 ? 'FFFFFFFF' : 'FF000000' }
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: rowIndex === 0 ? 'FF545454' : 'FFF5F5F5' }
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
// Merge
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
// Header row height
worksheet.getRow(currentRow).height = 30;
// Auto-adjust column widths
worksheet.columns.forEach(column => {
let maxLength = 5;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 0;
});
// Save
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,594 @@
{% extends "index.html" %}
{% block content %}
{% load static %}
{% load i18n %}
<div class="oh-wrapper mt-3">
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
align-items: center;'>
<h1 class="oh-main__titlebar-title fw-bold">
{% trans "Candidate Reports" %}
</h1>
<div style="display:inline-flex;">
<!-- Filter section -->
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
<div class="oh-main__titlebar oh-main__titlebar--right">
<div class="oh-main__titlebar-button-container">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn ml-2"
@click="open = !open"
onclick="event.preventDefault()"
>
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
<div id="filterCount"></div>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
x-show="open"
@click.outside="open = false"
style="display: none"
>
<div class="oh-dropdown__filter-body">
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Candidate" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans 'Name' %}</label>
{{ f.form.candidate_name }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Phone' %}</label>
{{ f.form.mobile }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Country' %}</label>
<select name="country" class="oh-select-2 w-100 country" id="country">
</select>
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Recruitment' %}</label>
{{ f.form.recruitment_id }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Job Position' %}</label>
{{ f.form.job_position_id }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Is Hired' %}?</label>
{{ f.form.hired }}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 mb-2">
<div class="oh-input-group">
<label class="oh-label">{% trans "Offer Status" %}</label>
{{f.form.offer_letter_status}}
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans 'Email' %}</label>
{{ f.form.email }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Gender' %}</label>
{{ f.form.gender }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'State' %}</label>
<select name="state" class="oh-select-2 w-100 country" id="state">
</select>
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Department' %}</label>
{{ f.form.job_position_id__department_id }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Stage Type' %}</label>
{{ f.form.stage_id__stage_type }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Is Canceled' %}?</label>
{{ f.form.canceled }}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Recruitment" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Recruitment" %}</label>
{{fr.form.title}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Managers" %}</label>
{{fr.form.recruitment_managers}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Start Date" %}</label>
{{fr.form.start_date}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Is Closed" %}</label>
<select name="closed" id="closed" class="oh-select oh-select-2 w-100">
<option value="unknown">{% trans "Unknown" %}</option>
<option value="true">{% trans "True" %}</option>
<option value="false" selected>{% trans "False" %}</option>
</select>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Job Position" %}</label>
{{fr.form.open_positions}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Company" %}</label>
{{fr.form.company_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "End Date" %}</label>
{{fr.form.end_date}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Is Published" %}</label>
{{fr.form.is_published}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Start Date From" %}</label>
{{fr.form.start_from}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans "Is Active" %}?</label>
{{fr.form.is_active}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Till End Date" %}</label>
{{fr.form.end_till}}
</div>
</div>
</div>
</div>
</div>
<div class="oh-accordion">
<div class="oh-accordion-header">{% trans "Onboarding" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Recruitment" %}</label>
{{fo.form.recruitment_id}}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Stage' %}</label>
{{ fo.form.stage_title }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Task' %}</label>
{{ fo.form.onboarding_task__task_title }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Company' %}</label>
{{ fo.form.recruitment_id__company_id }}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans 'Candidates' %}</label>
{{ fo.form.onboarding_task__candidates }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Stage Manager' %}</label>
{{ fo.form.employee_id }}
</div>
<div class="oh-input-group">
<label class="oh-label">{% trans 'Task Manager' %}</label>
{{ fo.form.onboarding_task__employee_id }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="oh-dropdown__filter-footer">
<button
type="button"
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
id="objective-filter-form-submit"
onclick="loadFilteredPivotData();"
>
{% trans "Filter" %}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Export Button -->
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
{% trans "Export Table" %}
</button>
</div>
</div>
<!-- Model Selection Dropdown -->
<span class="ms-1 fw-bold" style="font-size:16px;">{% trans "Choose Report" %} : </span>
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
<option value="candidate">{% trans "Candidate" %}</option>
<option value="recruitment">{% trans "Recruitment" %}</option>
<option value="onboarding">{% trans "Onboarding" %}</option>
</select>
<!-- Pivot Container -->
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
<div id="pivot-candidate" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-recruitment" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
<div id="pivot-onboarding" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
</div>
</div>
<script>
$(function () {
// Function to load pivot data dynamically
function loadPivotData(model) {
let url = `recruitment-pivot?model=${model}`;
// Hide all containers first
$(".pivot-wrapper").hide();
// Determine current container and row config
let containerId = "";
let rowsConfig = [];
if (model === "candidate") {
containerId = "pivot-candidate";
rowsConfig = ["Recruitment","Job Position","Department","Candidate","Gender","Email"];
} else if (model === "recruitment") {
containerId = "pivot-recruitment";
rowsConfig = ["Recruitment","Vacancy","Manager","Job Position","Start Date","End Date"];
} else if (model === "onboarding") {
containerId = "pivot-onboarding";
rowsConfig = ["Recruitment","Candidates","Stage","Stage Manager","Task","Task Manager","Company"];
}
// Show relevant container
$("#" + containerId).show();
$.getJSON(url, function (data) {
// Add Plotly renderers correctly
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
// Initialize pivot table with Plotly enabled
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table", // Default view as Table
onRefresh: function (config) {
let currentRenderer = config.rendererName;
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
$("#export-btn").show(); // Show button for tables
} else {
$("#export-btn").hide(); // Hide button for charts
}
// Hide fields from dropdown but keep them available in the table
let hiddenFields = [
"Vacancy",
];
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
setTimeout(function () {
$(".pvtAttrDropdown option").each(function () {
if (hiddenFields.includes($(this).text())) {
$(this).remove(); // Remove from selection
}
});
}, 10);
},
renderers: $.extend(
$.pivotUtilities.renderers,
plotlyRenderers // Adding Plotly renderers
)
});
});
}
window.loadFilteredPivotData =function loadFilteredPivotData() {
const selectedModel = $("#model-select").val();
const formData = $("#filterForm").serialize();
$(".pivot-wrapper").hide();
let containerId = "";
let rowsConfig = [];
if (selectedModel === "candidate") {
containerId = "pivot-candidate";
rowsConfig = ["Recruitment","Job Position","Department","Candidate","Gender","Email"];
} else if (selectedModel === "recruitment") {
containerId = "pivot-recruitment";
rowsConfig = ["Recruitment","Vacancy","Manager","Job Position","Start Date","End Date"];
} else if (selectedModel === "onboarding") {
containerId = "pivot-onboarding";
rowsConfig = ["Recruitment","Candidates","Stage","Stage Manager","Task","Task Manager","Company"];
}
$("#" + containerId).show();
$.getJSON(`recruitment-pivot?model=${selectedModel}&${formData}`, function (data) {
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
$("#" + containerId).pivotUI(data, {
rows: rowsConfig,
cols: [],
aggregatorName: "Count",
rendererName: "Table",
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
onRefresh: function (config) {
const currentRenderer = config.rendererName;
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
$("#export-btn").show();
} else {
$("#export-btn").hide();
}
}
});
});
}
// Initial load with all models
loadPivotData("candidate");
// Model selection change event
$("#model-select").on("change", function () {
let selectedModel = $(this).val();
loadPivotData(selectedModel); // Reload pivot data with selected model
});
// Export to Excel on button click
$("#export-btn").on("click", function () {
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
if (visiblePivot.length) {
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
}
});
// Export Function
async function exportTableToExcel(containerId, filename) {
let table = document.querySelector(`#${containerId} .pvtTable`);
if (!table) {
alert("No table found to export.");
return;
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Pivot Data");
const baseRow = 5;
const baseCol = 5;
let currentRow = baseRow;
// Add company details first (if not 'all')
if ('{{company}}' !== 'all') {
const companyDetails = {
name: "{{ company.company|escapejs }}",
address: "{{ company.address|escapejs }}",
country: "{{ company.country|escapejs }}",
state: "{{ company.state|escapejs }}",
city: "{{ company.city|escapejs }}",
zip: "{{ company.zip|escapejs }}"
};
function getBase64FromUrl(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}));
}
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
await getBase64FromUrl(logoUrl).then((base64) => {
const base64Data = base64.split(',')[1];
const imageId = workbook.addImage({
base64: base64Data,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: baseCol - 1, row: currentRow - 1 },
ext: { width: 80, height: 80 }
});
});
// Merge cells for company details text
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
companyTextCell.value = {
richText: [
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
]
};
companyTextCell.alignment = {
horizontal: 'left',
vertical: 'top',
wrapText: true
};
worksheet.getRow(currentRow).height = 80;
currentRow += 2; // Leave a blank row
}
// Add timestamp
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
new Date().toLocaleTimeString('en-US', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
});
const downloadCell = worksheet.getCell(currentRow, baseCol);
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
downloadCell.value = `Generated on: ${timestamp}`;
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
currentRow += 3; // Leave some rows before the table
// ------------------------
// Render pivot table
// ------------------------
const cellMap = {};
const allRows = Array.from(table.rows);
const lastRowIndex = allRows.length - 1;
allRows.forEach((row, rowIndex) => {
let colIndex = baseCol;
Array.from(row.cells).forEach((cell) => {
if (
cell.classList.contains("pvtTotal") ||
cell.classList.contains("pvtTotalLabel") ||
cell.classList.contains("pvtAggregator") ||
cell.classList.contains("pvtGrandTotal")
) return;
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
colIndex++;
}
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
const cellValue = cell.textContent.trim();
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
excelCell.value = cellValue;
const isHeader = rowIndex === 0;
const isLastRow = rowIndex === lastRowIndex;
excelCell.font = {
bold: isHeader || isLastRow,
size: isHeader ? 12 : 11,
color: {
argb: isHeader ? 'FFFFFFFF' :
isLastRow ? 'FF000000' :
'FF000000'
}
};
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: isHeader ? 'FF545454' :
isLastRow ? 'FFFFE599' : // light yellow
'FFF5F5F5'
}
};
excelCell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
excelCell.alignment = { horizontal: "center", vertical: "middle" };
// Merge
if (rowspan > 1 || colspan > 1) {
worksheet.mergeCells(
currentRow + rowIndex,
colIndex,
currentRow + rowIndex + rowspan - 1,
colIndex + colspan - 1
);
for (let r = 0; r < rowspan; r++) {
for (let c = 0; c < colspan; c++) {
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
}
}
} else {
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
}
colIndex++;
});
});
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
// Auto-adjust column widths
worksheet.columns.forEach(column => {
let maxLength = 2;
column.eachCell({ includeEmpty: true }, cell => {
const value = cell.value ? cell.value.toString() : '';
maxLength = Math.max(maxLength, value.length);
});
column.width = maxLength + 3;
});
// Save
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
});
</script>
{% endblock content %}

3
report/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

70
report/urls.py Normal file
View File

@@ -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'),
]
)

3
report/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
report/views/__init__.py Normal file
View File

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

254
report/views/pms_report.py Normal file
View File

@@ -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)

View File

@@ -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)