Initial commit
This commit is contained in:
154
fusion_payroll/static/src/css/roe_report.css
Normal file
154
fusion_payroll/static/src/css/roe_report.css
Normal file
@@ -0,0 +1,154 @@
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 0.45in 0.35in;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: Arial, sans-serif !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.page, .article, main {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
div[class*="page"], div[class*="article"] {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.roe-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.roe-wrapper .watermark {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
font-size: 125px;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
opacity: 0.07;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.roe-wrapper .roe-form {
|
||||
border: 2px solid black !important;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roe-wrapper .roe-form .header {
|
||||
background-color: #000 !important;
|
||||
color: #fff !important;
|
||||
padding: 5px 10px !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .roe-form table {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .roe-form table td {
|
||||
border: 1px solid black !important;
|
||||
padding: 4px !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .field-label {
|
||||
background-color: #666 !important;
|
||||
color: #fff !important;
|
||||
font-size: 10px !important;
|
||||
font-weight: bold !important;
|
||||
padding: 2px 4px !important;
|
||||
line-height: 1.1 !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .field-number {
|
||||
background-color: #000 !important;
|
||||
color: #fff !important;
|
||||
font-weight: bold !important;
|
||||
padding: 2px 4px !important;
|
||||
font-size: 10px !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .small-text {
|
||||
font-size: 9px !important;
|
||||
line-height: 1.1 !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .date-boxes {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 2px !important;
|
||||
align-items: center !important;
|
||||
margin-top: 3px !important;
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .date-box {
|
||||
border: 1px solid black !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
text-align: center !important;
|
||||
font-size: 11px !important;
|
||||
line-height: 20px !important;
|
||||
display: inline-block !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .date-label {
|
||||
font-size: 10px !important;
|
||||
margin-right: 2px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .earnings-table {
|
||||
width: 100% !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .earnings-table td {
|
||||
text-align: center !important;
|
||||
padding: 2px !important;
|
||||
border: 1px solid black !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .pp-col {
|
||||
width: 30px !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .footer-note {
|
||||
font-size: 9px !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.roe-wrapper .checkbox {
|
||||
display: inline-block !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
border: 1px solid black !important;
|
||||
margin-right: 4px !important;
|
||||
text-align: center !important;
|
||||
font-size: 11px !important;
|
||||
line-height: 14px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
266
fusion_payroll/static/src/js/payroll_report_action.js
Normal file
266
fusion_payroll/static/src/js/payroll_report_action.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
/**
|
||||
* PayrollReportAction - Main OWL component for payroll reports
|
||||
* Provides QuickBooks-style filtering, viewing, and export functionality.
|
||||
*/
|
||||
export class PayrollReportAction extends Component {
|
||||
static template = "fusion_payroll.PayrollReportAction";
|
||||
static props = {
|
||||
action: { type: Object },
|
||||
actionId: { type: [Number, Boolean], optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.notification = useService("notification");
|
||||
|
||||
// Get report model from action context
|
||||
this.reportModel = this.props.action.context?.report_model || 'payroll.report.paycheque.history';
|
||||
|
||||
this.state = useState({
|
||||
loading: true,
|
||||
reportName: '',
|
||||
columns: [],
|
||||
lines: [],
|
||||
options: {},
|
||||
dateFilterOptions: [],
|
||||
filterDateRange: true,
|
||||
filterEmployee: true,
|
||||
filterDepartment: false,
|
||||
selectedDateFilter: 'this_year',
|
||||
dateFrom: '',
|
||||
dateTo: '',
|
||||
showCustomDate: false,
|
||||
sortColumn: null,
|
||||
sortDirection: 'asc',
|
||||
expandedLines: {},
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.loadReportData();
|
||||
});
|
||||
}
|
||||
|
||||
async loadReportData(options = null) {
|
||||
this.state.loading = true;
|
||||
try {
|
||||
const result = await rpc("/payroll/report/get_report_data", {
|
||||
report_model: this.reportModel,
|
||||
options: options || this.state.options,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error("Report error:", result.error, result.traceback);
|
||||
this.notification.add(_t("Error loading report: %s", result.error), { type: "danger" });
|
||||
this.state.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.reportName = result.report_name;
|
||||
this.state.columns = result.columns || [];
|
||||
this.state.lines = result.lines || [];
|
||||
this.state.options = result.options || {};
|
||||
this.state.dateFilterOptions = result.date_filter_options || [];
|
||||
this.state.filterDateRange = result.filter_date_range !== undefined ? result.filter_date_range : true;
|
||||
this.state.filterEmployee = result.filter_employee !== undefined ? result.filter_employee : true;
|
||||
this.state.filterDepartment = result.filter_department !== undefined ? result.filter_department : false;
|
||||
|
||||
// Set date filter values
|
||||
if (result.options && result.options.date) {
|
||||
this.state.selectedDateFilter = result.options.date.filter;
|
||||
this.state.dateFrom = result.options.date.date_from;
|
||||
this.state.dateTo = result.options.date.date_to;
|
||||
this.state.showCustomDate = result.options.date.filter === 'custom';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading report:", error);
|
||||
this.notification.add(_t("Error loading report: %s", error.message || error), { type: "danger" });
|
||||
}
|
||||
this.state.loading = false;
|
||||
}
|
||||
|
||||
async onDateFilterChange(ev) {
|
||||
const filter = ev.target.value;
|
||||
this.state.selectedDateFilter = filter;
|
||||
this.state.showCustomDate = filter === 'custom';
|
||||
|
||||
if (filter !== 'custom') {
|
||||
await this.applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
async onDateFromChange(ev) {
|
||||
this.state.dateFrom = ev.target.value;
|
||||
}
|
||||
|
||||
async onDateToChange(ev) {
|
||||
this.state.dateTo = ev.target.value;
|
||||
}
|
||||
|
||||
async applyFilters() {
|
||||
const options = {
|
||||
...this.state.options,
|
||||
date: {
|
||||
filter: this.state.selectedDateFilter,
|
||||
date_from: this.state.dateFrom,
|
||||
date_to: this.state.dateTo,
|
||||
},
|
||||
};
|
||||
await this.loadReportData(options);
|
||||
}
|
||||
|
||||
sortByColumn(column) {
|
||||
if (!column.sortable) return;
|
||||
|
||||
if (this.state.sortColumn === column.field) {
|
||||
this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.state.sortColumn = column.field;
|
||||
this.state.sortDirection = 'asc';
|
||||
}
|
||||
|
||||
// Sort lines
|
||||
const sortedLines = [...this.state.lines].sort((a, b) => {
|
||||
const aVal = a.values?.[column.field] ?? '';
|
||||
const bVal = b.values?.[column.field] ?? '';
|
||||
|
||||
// Keep totals at top/bottom
|
||||
if (a.level < 0) return -1;
|
||||
if (b.level < 0) return 1;
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
comparison = aVal - bVal;
|
||||
} else {
|
||||
comparison = String(aVal).localeCompare(String(bVal));
|
||||
}
|
||||
|
||||
return this.state.sortDirection === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
|
||||
this.state.lines = sortedLines;
|
||||
}
|
||||
|
||||
async toggleLine(lineId) {
|
||||
if (this.state.expandedLines[lineId]) {
|
||||
delete this.state.expandedLines[lineId];
|
||||
} else {
|
||||
this.state.expandedLines[lineId] = true;
|
||||
// Load detail lines if needed
|
||||
await this.loadDetailLines(lineId);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDetailLines(lineId) {
|
||||
try {
|
||||
const result = await rpc("/payroll/report/get_detail_lines", {
|
||||
report_model: this.reportModel,
|
||||
line_id: lineId,
|
||||
options: this.state.options,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error("Detail lines error:", result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.detail_lines && result.detail_lines.length > 0) {
|
||||
// Insert detail lines after the parent line
|
||||
const lineIndex = this.state.lines.findIndex(l => l.id === lineId);
|
||||
if (lineIndex >= 0) {
|
||||
const newLines = [...this.state.lines];
|
||||
newLines.splice(lineIndex + 1, 0, ...result.detail_lines);
|
||||
this.state.lines = newLines;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading detail lines:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async exportXlsx() {
|
||||
const options = JSON.stringify(this.state.options);
|
||||
const url = `/payroll/report/export_xlsx?report_model=${this.reportModel}&options=${encodeURIComponent(options)}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
async exportPdf() {
|
||||
const options = JSON.stringify(this.state.options);
|
||||
const url = `/payroll/report/export_pdf?report_model=${this.reportModel}&options=${encodeURIComponent(options)}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
async openRecord(line) {
|
||||
if (line.model && line.res_id) {
|
||||
await this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: line.model,
|
||||
res_id: line.res_id,
|
||||
views: [[false, 'form']],
|
||||
target: 'current',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formatValue(value, columnType) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (columnType) {
|
||||
case 'monetary':
|
||||
return new Intl.NumberFormat('en-CA', {
|
||||
style: 'currency',
|
||||
currency: 'CAD',
|
||||
}).format(value);
|
||||
case 'float':
|
||||
return new Intl.NumberFormat('en-CA', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
case 'integer':
|
||||
return Math.round(value).toString();
|
||||
case 'date':
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString('en-CA');
|
||||
}
|
||||
return '';
|
||||
case 'percentage':
|
||||
return `${(value * 100).toFixed(2)}%`;
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
getLineClass(line) {
|
||||
const classes = [];
|
||||
if (line.class) {
|
||||
classes.push(line.class);
|
||||
}
|
||||
if (line.level < 0) {
|
||||
classes.push('o_payroll_report_total');
|
||||
}
|
||||
if (line.level > 0) {
|
||||
classes.push('o_payroll_report_detail');
|
||||
}
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.client',
|
||||
tag: 'fusion_payroll.report_hub',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register the component as a client action
|
||||
registry.category("actions").add("fusion_payroll.payroll_report_action", PayrollReportAction);
|
||||
48
fusion_payroll/static/src/js/report_hub.js
Normal file
48
fusion_payroll/static/src/js/report_hub.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class FusionPayrollReportHub extends Component {
|
||||
static template = "fusion_payroll.ReportHub";
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
}
|
||||
|
||||
async openReport(actionXmlId) {
|
||||
try {
|
||||
await this.actionService.doAction(actionXmlId);
|
||||
} catch (error) {
|
||||
console.error("Failed to open report:", actionXmlId, error);
|
||||
// Fallback: try to open directly with context
|
||||
const reportModels = {
|
||||
'fusion_payroll.action_payroll_report_paycheque_history': 'payroll.report.paycheque.history',
|
||||
'fusion_payroll.action_payroll_report_payroll_details': 'payroll.report.payroll.details',
|
||||
'fusion_payroll.action_payroll_report_tax_liability': 'payroll.report.tax.liability',
|
||||
'fusion_payroll.action_payroll_report_tax_payments': 'payroll.report.tax.payments',
|
||||
'fusion_payroll.action_payroll_report_tax_wage_summary': 'payroll.report.tax.wage.summary',
|
||||
'fusion_payroll.action_payroll_report_summary': 'payroll.report.summary',
|
||||
'fusion_payroll.action_payroll_report_summary_employee': 'payroll.report.summary.by.employee',
|
||||
'fusion_payroll.action_payroll_report_employee_directory': 'payroll.report.employee.directory',
|
||||
'fusion_payroll.action_payroll_report_time_off': 'payroll.report.time.off',
|
||||
'fusion_payroll.action_payroll_report_total_pay': 'payroll.report.total.pay',
|
||||
'fusion_payroll.action_payroll_report_total_cost': 'payroll.report.total.cost',
|
||||
'fusion_payroll.action_payroll_report_deductions': 'payroll.report.deductions',
|
||||
'fusion_payroll.action_payroll_report_workers_comp': 'payroll.report.workers.comp',
|
||||
};
|
||||
const reportModel = reportModels[actionXmlId];
|
||||
if (reportModel) {
|
||||
await this.actionService.doAction({
|
||||
type: 'ir.actions.client',
|
||||
tag: 'fusion_payroll.payroll_report_action',
|
||||
name: 'Report',
|
||||
context: { report_model: reportModel },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("fusion_payroll.report_hub", FusionPayrollReportHub);
|
||||
96
fusion_payroll/static/src/scss/payroll_report.scss
Normal file
96
fusion_payroll/static/src/scss/payroll_report.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
/* Payroll Report Styles */
|
||||
|
||||
.o_payroll_report_action {
|
||||
min-height: 100vh;
|
||||
|
||||
.o_payroll_report_table {
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
|
||||
&.o_payroll_report_total {
|
||||
font-weight: 600;
|
||||
border-top: 2px solid;
|
||||
}
|
||||
|
||||
&.o_payroll_report_detail {
|
||||
font-size: 0.9em;
|
||||
|
||||
td:first-child {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&.text-end {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Filter Card - Let Bootstrap handle background */
|
||||
.card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Date Inputs */
|
||||
.form-control-sm {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.form-select-sm {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Report Hub Styles */
|
||||
.o_fusion_report_hub {
|
||||
.card {
|
||||
transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.o_payroll_report_action {
|
||||
background-color: white;
|
||||
|
||||
.btn, .form-control, .form-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o_payroll_report_table {
|
||||
font-size: 10pt;
|
||||
|
||||
th, td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
fusion_payroll/static/src/xml/payroll_report_templates.xml
Normal file
162
fusion_payroll/static/src/xml/payroll_report_templates.xml
Normal file
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_payroll.PayrollReportAction">
|
||||
<div class="o_action o_payroll_report_action">
|
||||
<!-- Loading Spinner -->
|
||||
<div t-if="state.loading" class="d-flex justify-content-center align-items-center" style="min-height: 400px;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-else="" class="container-fluid py-3">
|
||||
<!-- Header -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn btn-link p-0 me-3" t-on-click="goBack">
|
||||
<i class="fa fa-chevron-left"/> Back
|
||||
</button>
|
||||
<h2 class="mb-0">
|
||||
<t t-out="state.reportName"/>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Export Buttons -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown">
|
||||
Export <i class="fa fa-caret-down ms-1"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click.prevent="exportXlsx">
|
||||
<i class="fa fa-file-excel-o me-2"/> Export to Excel
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" t-on-click.prevent="exportPdf">
|
||||
<i class="fa fa-file-pdf-o me-2"/> Export to PDF
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Row -->
|
||||
<div class="row mb-3" t-if="state.filterDateRange">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body py-2">
|
||||
<div class="row align-items-center">
|
||||
<!-- Date Filter Dropdown -->
|
||||
<div class="col-auto">
|
||||
<select class="form-select form-select-sm"
|
||||
t-on-change="onDateFilterChange"
|
||||
t-att-value="state.selectedDateFilter">
|
||||
<t t-foreach="state.dateFilterOptions" t-as="option" t-key="option[0]">
|
||||
<option t-att-value="option[0]"
|
||||
t-att-selected="option[0] === state.selectedDateFilter">
|
||||
<t t-out="option[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date From -->
|
||||
<div class="col-auto">
|
||||
<input type="date"
|
||||
class="form-control form-control-sm"
|
||||
t-att-value="state.dateFrom"
|
||||
t-on-change="onDateFromChange"/>
|
||||
</div>
|
||||
|
||||
<!-- Date To -->
|
||||
<div class="col-auto">
|
||||
<input type="date"
|
||||
class="form-control form-control-sm"
|
||||
t-att-value="state.dateTo"
|
||||
t-on-change="onDateToChange"/>
|
||||
</div>
|
||||
|
||||
<!-- Apply Button (for custom dates) -->
|
||||
<div class="col-auto" t-if="state.showCustomDate">
|
||||
<button class="btn btn-primary btn-sm" t-on-click="applyFilters">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Customize Button -->
|
||||
<div class="col-auto ms-auto">
|
||||
<button class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fa fa-sliders me-1"/> Customize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0 o_payroll_report_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<t t-foreach="state.columns" t-as="column" t-key="column.field">
|
||||
<th t-att-class="column.sortable ? 'cursor-pointer user-select-none' : ''"
|
||||
t-on-click="() => this.sortByColumn(column)">
|
||||
<t t-out="column.name"/>
|
||||
<t t-if="column.sortable">
|
||||
<i t-if="state.sortColumn === column.field"
|
||||
t-att-class="'fa ms-1 ' + (state.sortDirection === 'asc' ? 'fa-sort-asc' : 'fa-sort-desc')"/>
|
||||
<i t-else="" class="fa fa-sort ms-1 text-muted"/>
|
||||
</t>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-if="state.lines.length === 0">
|
||||
<tr>
|
||||
<td t-att-colspan="state.columns.length" class="text-center text-muted py-4">
|
||||
There are no results matching the criteria.
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-foreach="state.lines" t-as="line" t-key="line.id">
|
||||
<tr t-att-class="this.getLineClass(line)"
|
||||
t-on-click="() => line.model and line.res_id and this.openRecord(line)"
|
||||
style="cursor: pointer;">
|
||||
<t t-foreach="state.columns" t-as="column" t-key="column.field">
|
||||
<td t-att-class="column.type === 'monetary' or column.type === 'float' ? 'text-end' : ''">
|
||||
<!-- Unfoldable indicator -->
|
||||
<t t-if="column_index === 0 and line.unfoldable">
|
||||
<i t-att-class="'fa me-1 ' + (state.expandedLines[line.id] ? 'fa-caret-down' : 'fa-caret-right')"
|
||||
t-on-click.stop="() => this.toggleLine(line.id)"/>
|
||||
</t>
|
||||
<t t-out="this.formatValue(line.values?.[column.field], column.type)"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
185
fusion_payroll/static/src/xml/report_hub.xml
Normal file
185
fusion_payroll/static/src/xml/report_hub.xml
Normal file
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_payroll.ReportHub">
|
||||
<div class="o_action o_fusion_report_hub">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-1">
|
||||
<i class="fa fa-bar-chart me-2 text-primary"/>Payroll Reports
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Select a report to view payroll data and analytics</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reports Grid -->
|
||||
<div class="row g-3">
|
||||
<!-- Left Column -->
|
||||
<div class="col-md-6">
|
||||
<!-- Paycheque History -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_paycheque_history')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-file-text-o fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Paycheque History</h6>
|
||||
<small class="text-muted">View all paycheques by date</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deductions and Contributions -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_deductions')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-minus-circle fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Deductions and Contributions</h6>
|
||||
<small class="text-muted">Employee and company deductions</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Summary by Employee -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_summary_employee')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-users fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Summary by Employee</h6>
|
||||
<small class="text-muted">Pivot by employee</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Item List -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_employee_directory')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-address-book fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Item List</h6>
|
||||
<small class="text-muted">Employee pay rates and status</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Details -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_payroll_details')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-list-alt fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Details</h6>
|
||||
<small class="text-muted">Detailed breakdown per pay period</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Summary -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_summary')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-table fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Summary</h6>
|
||||
<small class="text-muted">Summary with all components</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Off -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_time_off')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-calendar-check-o fa-lg text-primary me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Time Off</h6>
|
||||
<small class="text-muted">Vacation and leave balances</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-md-6">
|
||||
<!-- Payroll Tax Liability -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_tax_liability')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-balance-scale fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Tax Liability</h6>
|
||||
<small class="text-muted">Tax amounts owed vs paid</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Tax Payments -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_tax_payments')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-credit-card fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Tax Payments</h6>
|
||||
<small class="text-muted">History of remittance payments</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Payroll Cost -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_total_cost')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-calculator fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Total Payroll Cost</h6>
|
||||
<small class="text-muted">All payroll costs breakdown</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Pay -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_total_pay')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-money fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Total Pay</h6>
|
||||
<small class="text-muted">Pay by type per employee</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payroll Tax and Wage Summary -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_tax_wage_summary')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-pie-chart fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Payroll Tax and Wage Summary</h6>
|
||||
<small class="text-muted">Wages and tax amounts</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workers' Compensation -->
|
||||
<div class="card mb-2" style="cursor: pointer;" t-on-click="() => this.openReport('fusion_payroll.action_payroll_report_workers_comp')">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<i class="fa fa-shield fa-lg text-success me-3"/>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0">Workers' Compensation</h6>
|
||||
<small class="text-muted">WCB wages by province</small>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user