/** @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);