[ADD] REPORT: Add reporting module for Horilla (#750)
This commit is contained in:
0
report/__init__.py
Normal file
0
report/__init__.py
Normal file
3
report/admin.py
Normal file
3
report/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
18
report/apps.py
Normal file
18
report/apps.py
Normal 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
|
||||
0
report/migrations/__init__.py
Normal file
0
report/migrations/__init__.py
Normal file
3
report/models.py
Normal file
3
report/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
90
report/sidebar.py
Normal file
90
report/sidebar.py
Normal 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")
|
||||
|
||||
381
report/templates/report/asset_report.html
Normal file
381
report/templates/report/asset_report.html
Normal 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 %}
|
||||
524
report/templates/report/attendance_report.html
Normal file
524
report/templates/report/attendance_report.html
Normal 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 %}
|
||||
411
report/templates/report/employee_report.html
Normal file
411
report/templates/report/employee_report.html
Normal 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 %}
|
||||
587
report/templates/report/leave_report.html
Normal file
587
report/templates/report/leave_report.html
Normal 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 %}
|
||||
583
report/templates/report/payroll_report.html
Normal file
583
report/templates/report/payroll_report.html
Normal 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 %}
|
||||
526
report/templates/report/pms_report.html
Normal file
526
report/templates/report/pms_report.html
Normal 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 %}
|
||||
594
report/templates/report/recruitment_report.html
Normal file
594
report/templates/report/recruitment_report.html
Normal 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
3
report/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
70
report/urls.py
Normal file
70
report/urls.py
Normal 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
3
report/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
report/views/__init__.py
Normal file
0
report/views/__init__.py
Normal file
78
report/views/asset_report.py
Normal file
78
report/views/asset_report.py
Normal 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)
|
||||
139
report/views/attendance_report.py
Normal file
139
report/views/attendance_report.py
Normal 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"
|
||||
|
||||
59
report/views/employee_report.py
Normal file
59
report/views/employee_report.py
Normal 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)
|
||||
133
report/views/leave_report.py
Normal file
133
report/views/leave_report.py
Normal 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)
|
||||
254
report/views/payroll_report.py
Normal file
254
report/views/payroll_report.py
Normal 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
254
report/views/pms_report.py
Normal 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)
|
||||
127
report/views/recruitment_report.py
Normal file
127
report/views/recruitment_report.py
Normal 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)
|
||||
Reference in New Issue
Block a user