From 5e617d3ff8f0e1919532025f2c6f87c74a0f315f Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Wed, 1 Jul 2026 17:22:14 +0530 Subject: [PATCH] payroll changes --- .../pqgrid_batch_payslip.js | 335 +++++++++--------- .../hr_payroll/views/ftp_payslip.xml | 1 - .../views/hr_payroll_structure_views.xml | 2 +- .../hr_payroll_extended/__manifest__.py | 11 +- .../hr_payroll_extended/models/__init__.py | 2 + .../models/hr_payslip_employees.py | 85 +++++ .../models/hr_work_entry.py | 15 + .../src/js/consolidated_payslip_grid_patch.js | 215 +++++++++++ .../views/hr_payslip_employees_views.xml | 91 +++++ .../views/hr_work_entry_views.xml | 51 +++ 10 files changed, 639 insertions(+), 169 deletions(-) create mode 100644 addons_extensions/hr_payroll_extended/models/hr_payslip_employees.py create mode 100644 addons_extensions/hr_payroll_extended/models/hr_work_entry.py create mode 100644 addons_extensions/hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js create mode 100644 addons_extensions/hr_payroll_extended/views/hr_payslip_employees_views.xml create mode 100644 addons_extensions/hr_payroll_extended/views/hr_work_entry_views.xml diff --git a/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js index e76225746..17882d048 100644 --- a/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js +++ b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { standardWidgetProps } from "@web/views/widgets/standard_widget_props"; -import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; -import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; -import { loadJS, loadCSS } from "@web/core/assets"; -import { rpc } from "@web/core/network/rpc"; +import {standardWidgetProps} from "@web/views/widgets/standard_widget_props"; +import {Component, onMounted, useRef, useState, onWillStart, onWillUpdateProps} from "@odoo/owl"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; +import {loadJS, loadCSS} from "@web/core/assets"; +import {rpc} from "@web/core/network/rpc"; export class ConsolidatedPayslipGrid extends Component { static props = { @@ -34,6 +34,12 @@ export class ConsolidatedPayslipGrid extends Component { console.error("Grid element not found"); } }); + + onWillUpdateProps(async (nextProps) => { + if (nextProps.record.data.state !== this.props.record.data.state) { + await this.loadGrid(); + } + }); } async loadDependencies() { @@ -117,117 +123,106 @@ export class ConsolidatedPayslipGrid extends Component { } async renderGrid() { - if (!this.gridRef.el) return; - const columns = await this.getColumns(); - const agg = pq.aggregate; + if (!this.gridRef.el) return; + const columns = await this.getColumns(); + const agg = pq.aggregate; - // Define custom aggregate functions - agg.sum_ = function(arr, col) { - return " " + agg.sum(arr, col).toFixed(2).toString(); - }; - - const groupModel = { - on: true, - //dataIndx: ["employee_code",""], - collapsed: [false, true], - merge: true, - showSummary: [true, true], - grandSummary: true, - render: () => "" // hides the group title row text - }; - - - const gridOptions = { - - selectionModel: { type: 'row' }, - width: "100%", - height: "100%", - groupModel: groupModel, - editable: true, - stripeRows:false, - editModel: { saveKey: $.ui.keyCode.ENTER }, - filterModel: {on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1}, - dataModel: {data: this.state.rows, location: "local", sorting: "local", paging: "local"}, - cellSave: function (evt, ui) { - const payload = { - id: ui.rowData.id, - field: ui.dataIndx, - value: ui.newVal + // Define custom aggregate functions + agg.sum_ = function (arr, col) { + return " " + agg.sum(arr, col).toFixed(2).toString(); }; - updateData(payload); - }, - menuIcon: true, - menuUI:{tabs: ['hideCols']}, - colModel: columns, - postRenderInterval: -1, - toolbar: { - items: [ - { - type: 'button', - label: 'Refresh', - icon: 'ui-icon-refresh', - listener: () => this.loadGrid() - }, - { - type: 'button', - label: 'Save Changes', - icon: 'ui-icon-disk', - listener: () => this.saveChanges() - }, - { - type: 'button', - label: 'Recalculate LOP', - icon: 'ui-icon-calculator', - listener: () => this.recalculateLOP() - }, - { - type: 'select', - label: 'Format: ', - attr: 'id="export_format"', - options: [{ xlsx: 'Excel', csv: 'Csv', htm: 'Html', json: 'Json'}] - }, - { - type: 'button', - label: "Export", - icon: 'ui-icon-arrowthickstop-1-s', - listener: function () { - var format = $("#export_format").val(), - blob = this.exportData({ - //url: "/pro/demos/exportData", - format: format, - render: true - }); - if(typeof blob === "string"){ - blob = new Blob([blob]); + const groupModel = { + on: true, + //dataIndx: ["employee_code",""], + collapsed: [false, true], + merge: true, + showSummary: [true, true], + grandSummary: true, + render: () => "" // hides the group title row text + }; + + + const gridOptions = { + + selectionModel: {type: 'row'}, + width: "100%", + height: "100%", + groupModel: groupModel, + editable: true, + stripeRows: false, + editModel: {saveKey: $.ui.keyCode.ENTER}, + filterModel: {on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1}, + dataModel: {data: this.state.rows, location: "local", sorting: "local", paging: "local"}, + cellSave: function (evt, ui) { + const payload = { + id: ui.rowData.id, + field: ui.dataIndx, + value: ui.newVal + }; + updateData(payload); + }, + menuIcon: true, + menuUI: {tabs: ['hideCols']}, + colModel: columns, + postRenderInterval: -1, + toolbar: { + items: [ + { + type: 'button', + label: 'Refresh', + icon: 'ui-icon-refresh', + listener: () => this.loadGrid() + }, + { + type: 'select', + label: 'Format: ', + attr: 'id="export_format"', + options: [{xlsx: 'Excel', csv: 'Csv', htm: 'Html', json: 'Json'}] + }, + { + type: 'button', + label: "Export", + icon: 'ui-icon-arrowthickstop-1-s', + listener: function () { + + var format = $("#export_format").val(), + blob = this.exportData({ + //url: "/pro/demos/exportData", + format: format, + render: true + }); + if (typeof blob === "string") { + blob = new Blob([blob]); + } + saveAs(blob, "PaySheet." + format); } - saveAs(blob, "PaySheet."+ format ); - } + }, + ] + }, + }; + + function updateData(data) { + $.ajax({ + url: "/slip/update", + type: "POST", + contentType: "application/json", + data: JSON.stringify(data), + success: function (response) { + console.log("Update successful:", response); }, - ] - }, - }; - function updateData(data){ - $.ajax({ - url: "/slip/update", - type: "POST", - contentType: "application/json", - data: JSON.stringify(data), - success: function (response) { - console.log("Update successful:", response); - }, - error: function (xhr) { - console.error("Update failed:", xhr.responseText); - } - }); - }; + error: function (xhr) { + console.error("Update failed:", xhr.responseText); + } + }); + }; - // Apply CSS and initialize grid - $(this.gridRef.el) - .css({ height: '600px', width: '100%' }) - .pqGrid(gridOptions) - .pqGrid("refreshDataAndView"); + // Apply CSS and initialize grid + $(this.gridRef.el) + .css({height: '600px', width: '100%'}) + .pqGrid(gridOptions) + .pqGrid("refreshDataAndView"); } async refreshDataAndView() { @@ -293,14 +288,14 @@ export class ConsolidatedPayslipGrid extends Component { } async getColumns() { - const subCols = await this.getSubgridColumns(); + const subCols = await this.getSubgridColumns(); - return [ + const columns = [ { title: "Employee", dataIndx: "employee", width: 200, - filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, + filter: {type: 'textbox', condition: 'contain', listeners: ['keyup']}, editable: false, menuIcon: true, menuInHide: true @@ -309,7 +304,7 @@ export class ConsolidatedPayslipGrid extends Component { title: "Employee ID", dataIndx: "employee_code", width: 200, - filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, + filter: {type: 'textbox', condition: 'contain', listeners: ['keyup']}, editable: false, menuInHide: true }, @@ -317,7 +312,7 @@ export class ConsolidatedPayslipGrid extends Component { title: "Department", dataIndx: "department", width: 150, - filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, + filter: {type: 'textbox', condition: 'contain', listeners: ['keyup']}, editable: false }, { @@ -372,7 +367,7 @@ export class ConsolidatedPayslipGrid extends Component { width: 80, dataType: "integer", editable: (rowData) => rowData.state === 'draft', - summary: { type: "sum_" } + summary: {type: "sum_"} }, { title: "Sick Leave Balance", @@ -380,7 +375,7 @@ export class ConsolidatedPayslipGrid extends Component { width: 100, dataType: "float", editable: false, - summary: { type: "sum_" }, + summary: {type: "sum_"}, format: "##,##0.00" }, { @@ -389,7 +384,7 @@ export class ConsolidatedPayslipGrid extends Component { width: 100, dataType: "float", editable: false, - summary: { type: "sum_" }, + summary: {type: "sum_"}, format: "##,##0.00" }, { @@ -398,7 +393,7 @@ export class ConsolidatedPayslipGrid extends Component { width: 100, dataType: "float", editable: false, - summary: { type: "sum_" }, + summary: {type: "sum_"}, format: "##,##0.00" }, // { @@ -438,67 +433,77 @@ export class ConsolidatedPayslipGrid extends Component { return `${state}`; } }, - ...subCols, - { - title: "View", - width: 120, - editable: false, - summary:false, - render: function (ui) { - return "" - }, - postRender: function (ui) { - var grid = this, - $cell = grid.getCell(ui); - $cell.find(".row-btn-view") - .button({ icons: { primary: 'ui-icon-extlink'} }) - .on("click", async function (evt) { - const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip','action_open_payslips',[ui.rowData.id]) + ...subCols, + { + title: "View", + width: 120, + editable: false, + summary: false, + render: function (ui) { + return "" + }, + postRender: function (ui) { + var grid = this, + $cell = grid.getCell(ui); + $cell.find(".row-btn-view") + .button({icons: {primary: 'ui-icon-extlink'}}) + .on("click", async function (evt) { + const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip', 'action_open_payslips', [ui.rowData.id]) // res.views = [[false, "form"]], await odoo.__WOWL_DEBUG__.root.actionService.doAction(res) }); - } + } - }, - { - title: "Edit", - width: 120, - editable: false, - render: function (ui) { - return "" - }, - postRender: function (ui) { + }, + + ]; + if (this.props.record.data.state !== "paid") { + columns.push( + { + title: "Edit", + width: 120, + editable: false, + render: function (ui) { + if (ui.rowData.state == 'paid') { + return "" + } + return "" + }, + postRender: function (ui) { var grid = this, $cell = grid.getCell(ui); $cell.find(".row-btn-edit") - .button({ icons: { primary: 'ui-icon-pencil'} }) + .button({icons: {primary: 'ui-icon-pencil'}}) .on("click", async function (evt) { - const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip','action_edit_payslip_lines',[ui.rowData.id]) - res.views = [[false, "form"]], - await odoo.__WOWL_DEBUG__.root.actionService.doAction(res) - }); + const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip', 'action_edit_payslip_lines', [ui.rowData.id]) + res.views = [[false, "form"]], + await odoo.__WOWL_DEBUG__.root.actionService.doAction(res) + }); } - } - ]; + } + ) + } + + return columns } async getSubgridColumns() { const response = await this.orm.call( - "hr.payslip.run", - "sub_columns", - [this.state.payslipRunId] - ); - return response || []; + "hr.payslip.run", + "sub_columns", + [this.state.payslipRunId] + ); + return response || []; } async refreshGrid() { try { await this.loadGridData(); $(this.gridRef.el).pqGrid("refreshDataAndView"); - this.notification.add("Data refreshed successfully", { type: 'success' }); + this.notification.add("Data refreshed successfully", {type: 'success'}); } catch (error) { console.error("Error refreshing grid:", error); - this.notification.add("Error refreshing data", { type: 'danger' }); + this.notification.add("Error refreshing data", {type: 'danger'}); } } @@ -513,10 +518,10 @@ export class ConsolidatedPayslipGrid extends Component { [this.state.payslipRunId, updatedData] ); await this.refreshGrid(); - this.notification.add("Changes saved successfully", { type: 'success' }); + this.notification.add("Changes saved successfully", {type: 'success'}); } catch (error) { console.error("Error saving data:", error); - this.notification.add("Error saving changes", { type: 'danger' }); + this.notification.add("Error saving changes", {type: 'danger'}); } } @@ -528,10 +533,10 @@ export class ConsolidatedPayslipGrid extends Component { [this.state.payslipRunId] ); await this.refreshGrid(); - this.notification.add("LOP days recalculated successfully", { type: 'success' }); + this.notification.add("LOP days recalculated successfully", {type: 'success'}); } catch (error) { console.error("Error recalculating LOP:", error); - this.notification.add("Error recalculating LOP", { type: 'danger' }); + this.notification.add("Error recalculating LOP", {type: 'danger'}); } } @@ -543,10 +548,10 @@ export class ConsolidatedPayslipGrid extends Component { [this.state.payslipRunId] ); await this.refreshGrid(); - this.notification.add("All records validated successfully", { type: 'success' }); + this.notification.add("All records validated successfully", {type: 'success'}); } catch (error) { console.error("Error validating records:", error); - this.notification.add("Error validating records", { type: 'danger' }); + this.notification.add("Error validating records", {type: 'danger'}); } } } diff --git a/addons_extensions/hr_payroll/views/ftp_payslip.xml b/addons_extensions/hr_payroll/views/ftp_payslip.xml index ee8580c40..6723c9e3b 100644 --- a/addons_extensions/hr_payroll/views/ftp_payslip.xml +++ b/addons_extensions/hr_payroll/views/ftp_payslip.xml @@ -122,7 +122,6 @@ Gross Salary - Total Deduction diff --git a/addons_extensions/hr_payroll/views/hr_payroll_structure_views.xml b/addons_extensions/hr_payroll/views/hr_payroll_structure_views.xml index 72693c18e..46123faac 100644 --- a/addons_extensions/hr_payroll/views/hr_payroll_structure_views.xml +++ b/addons_extensions/hr_payroll/views/hr_payroll_structure_views.xml @@ -74,10 +74,10 @@ + - diff --git a/addons_extensions/hr_payroll_extended/__manifest__.py b/addons_extensions/hr_payroll_extended/__manifest__.py index 4babe4137..d5dc34fc8 100644 --- a/addons_extensions/hr_payroll_extended/__manifest__.py +++ b/addons_extensions/hr_payroll_extended/__manifest__.py @@ -5,14 +5,21 @@ 'installable': True, 'application': True, 'depends': [ - 'hr_payroll' + 'hr_payroll', + 'consolidated_batch_payslip', + 'hr_work_entry_holidays' ], 'data': [ 'data/hr_salary_advance_sequence.xml', 'security/ir.model.access.csv', + 'views/hr_payslip_employees_views.xml', 'views/hr_salary_advance_views.xml', - 'views/menus.xml' + 'views/menus.xml', + 'views/hr_work_entry_views.xml', ], 'assets': { + 'web.assets_backend': [ + 'hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js', + ], }, } diff --git a/addons_extensions/hr_payroll_extended/models/__init__.py b/addons_extensions/hr_payroll_extended/models/__init__.py index 1f3710b17..b56ed82c1 100644 --- a/addons_extensions/hr_payroll_extended/models/__init__.py +++ b/addons_extensions/hr_payroll_extended/models/__init__.py @@ -1 +1,3 @@ +from . import hr_work_entry from . import hr_salary_advance +from . import hr_payslip_employees diff --git a/addons_extensions/hr_payroll_extended/models/hr_payslip_employees.py b/addons_extensions/hr_payroll_extended/models/hr_payslip_employees.py new file mode 100644 index 000000000..da5e41f30 --- /dev/null +++ b/addons_extensions/hr_payroll_extended/models/hr_payslip_employees.py @@ -0,0 +1,85 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class HrPayslipEmployees(models.TransientModel): + _inherit = "hr.payslip.employees" + + conflict_work_entry_ids = fields.Many2many( + "hr.work.entry", + compute="_compute_conflict_work_entry_ids", + inverse="_inverse_conflict_work_entry_ids", + string="Work Entry Conflicts", + readonly=False, + ) + conflict_work_entry_count = fields.Integer( + compute="_compute_conflict_work_entry_ids", + string="Conflict Count", + ) + timeoff_differ_ids = fields.Many2many( + "hr.leave", + compute="_compute_differ_timeoff_ids", + inverse="_inverse_differ_timeoff_ids", + string="Work Entry Conflicts", + readonly=False, + ) + + def _get_generation_dates(self): + self.ensure_one() + payslip_run = self.env["hr.payslip.run"].browse(self.env.context.get("active_id")).exists() + if payslip_run: + return payslip_run.date_start, payslip_run.date_end + return ( + fields.Date.to_date(self.env.context.get("default_date_start")), + fields.Date.to_date(self.env.context.get("default_date_end")), + ) + + @api.depends("employee_ids") + def _compute_differ_timeoff_ids(self): + DifferLeaves = self.env["hr.leave"] + for wizard in self: + date_start, date_end = wizard._get_generation_dates() + if not wizard.employee_ids or not date_start or not date_end: + conflicts = DifferLeaves + else: + conflicts = DifferLeaves.search([ + ("employee_id", "in", wizard.employee_ids.ids), + ("payslip_state", "=", "blocked"), + ("date_from", "<=", date_end + relativedelta(days=1)), + ("date_to", ">=", date_start + relativedelta(days=-1)), + ]) + wizard.timeoff_differ_ids = conflicts + + def _inverse_differ_timeoff_ids(self): + return + + @api.depends("employee_ids") + def _compute_conflict_work_entry_ids(self): + WorkEntry = self.env["hr.work.entry"] + for wizard in self: + date_start, date_end = wizard._get_generation_dates() + if not wizard.employee_ids or not date_start or not date_end: + conflicts = WorkEntry + else: + conflicts = WorkEntry.search([ + ("employee_id", "in", wizard.employee_ids.ids), + ("state", "=", "conflict"), + ("date_start", "<=", date_end + relativedelta(days=1)), + ("date_stop", ">=", date_start + relativedelta(days=-1)), + ]) + wizard.conflict_work_entry_ids = conflicts + wizard.conflict_work_entry_count = len(conflicts) + + def _inverse_conflict_work_entry_ids(self): + return + + def action_refresh_conflict_work_entries(self): + self.invalidate_recordset(["conflict_work_entry_ids", "conflict_work_entry_count"]) + return { + "type": "ir.actions.act_window", + "res_model": self._name, + "res_id": self.id, + "view_mode": "form", + "target": "new", + } diff --git a/addons_extensions/hr_payroll_extended/models/hr_work_entry.py b/addons_extensions/hr_payroll_extended/models/hr_work_entry.py new file mode 100644 index 000000000..ed064fe35 --- /dev/null +++ b/addons_extensions/hr_payroll_extended/models/hr_work_entry.py @@ -0,0 +1,15 @@ +from odoo import models + +class HrWorkEntry(models.Model): + _inherit = 'hr.work.entry' + + def action_open_conflict(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": "Work Entry Conflict", + "res_model": "hr.work.entry", + "res_id": self.id, + "view_mode": "form", + "target": "new", # popup + } \ No newline at end of file diff --git a/addons_extensions/hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js b/addons_extensions/hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js new file mode 100644 index 000000000..f9fa6840b --- /dev/null +++ b/addons_extensions/hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js @@ -0,0 +1,215 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { useService } from "@web/core/utils/hooks"; +import { ConsolidatedPayslipGrid } from "@consolidated_batch_payslip/components/pqgrid_batch_payslip/pqgrid_batch_payslip"; + +patch(ConsolidatedPayslipGrid.prototype, { + setup() { + super.setup(...arguments); + this.notification = useService("notification"); + }, + + async loadGrid() { + try { + if (!this.state.payslipRunId) { + return; + } + + const records = await this.orm.call( + "hr.payslip.run", + "get_consolidated_attendance_data", + [this.state.payslipRunId] + ); + this.state.rows = records.map((rec) => { + const row = { + id: rec.id, + employee_id: rec.employee_id[0], + employee: rec.employee_id[1], + employee_code: rec.employee_code || "N/A", + department: rec.department_id ? rec.department_id[1] : "N/A", + total_days: rec.total_days || 0, + worked_days: rec.worked_days || 0, + attendance_days: rec.attendance_days || 0, + leave_days: rec.leave_days || 0, + lop_days: rec.lop_days || 0, + sick_leave_balance: rec.sick_leave_balance || 0, + casual_leave_balance: rec.casual_leave_balance || 0, + privilege_leave_balance: rec.privilege_leave_balance || 0, + sick_leave_taken: rec.sick_leave_taken || 0, + casual_leave_taken: rec.casual_leave_taken || 0, + privilege_leave_taken: rec.privilege_leave_taken || 0, + state: rec.state || "draft", + doj: rec.doj, + bank: rec.bank, + birthday: rec.birthday, + lines: rec.lines || [], + }; + for (const line of rec.lines || []) { + if (line.code) { + row[line.code] = line.amount; + } + } + return row; + }); + + const grid = this._getGridInstance(); + if (grid) { + grid.option("dataModel.data", this.state.rows); + grid.refreshDataAndView(); + this._wireToolbarActions(); + } else { + await this.renderGrid(); + } + } catch (error) { + console.error("Error loading data:", error); + this.showNotification("Error loading grid data", "danger"); + } + }, + + async renderGrid() { + await super.renderGrid(...arguments); + this._wireToolbarActions(); + }, + + async refreshGrid() { + await this.loadGrid(); + this.showNotification("Data refreshed successfully"); + }, + + async saveChanges() { + const grid = this._getGridInstance(); + const updatedData = grid.option("dataModel.data"); + + try { + await this.orm.call( + "hr.payslip.run", + "save_consolidated_attendance_data", + [this.state.payslipRunId, updatedData] + ); + await this.refreshGrid(); + this.showNotification("Changes saved successfully"); + } catch (error) { + console.error("Error saving data:", error); + this.showNotification("Error saving changes", "danger"); + } + }, + + async recalculateLOP() { + try { + await this.orm.call("hr.payslip.run", "recalculate_lop_days", [this.state.payslipRunId]); + await this.refreshGrid(); + this.showNotification("LOP days recalculated successfully"); + } catch (error) { + console.error("Error recalculating LOP:", error); + this.showNotification("Error recalculating LOP", "danger"); + } + }, + + showNotification(message, type = "success") { + if (this.notification) { + this.notification.add(message, { type }); + } else { + console.log(`${type}: ${message}`); + } + }, + + _wireToolbarActions() { + if (!this._getGridInstance()) { + return; + } + const $grid = $(this.gridRef.el); + $grid.find("button").filter((_, button) => button.textContent.trim() === "Refresh") + .off("click.hr_payroll_extended") + .on("click.hr_payroll_extended", (ev) => { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this.refreshGrid(); + }); + $grid.find("button").filter((_, button) => button.textContent.trim() === "Export") + .off("click.hr_payroll_extended") + .on("click.hr_payroll_extended", (ev) => { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this._exportGridData(); + }); + }, + + _exportGridData() { + const format = $("#export_format").val() || "csv"; + const filename = `PaySheet.${format === "xlsx" ? "xls" : format}`; + try { + const blob = $(this.gridRef.el).pqGrid("exportData", { format, render: true }); + if (blob) { + this._saveBlob(blob, `PaySheet.${format}`); + return; + } + } catch (error) { + console.warn("pqGrid exportData failed, using fallback export.", error); + } + + const grid = this._getGridInstance(); + const rows = grid.option("dataModel.data") || []; + const columns = (grid.option("colModel") || []).filter((col) => col.dataIndx); + const content = this._buildExportContent(format, rows, columns); + this._saveBlob(content, filename); + }, + + _buildExportContent(format, rows, columns) { + if (format === "json") { + return new Blob([JSON.stringify(rows, null, 2)], { type: "application/json;charset=utf-8" }); + } + if (format === "htm" || format === "xlsx") { + const header = columns.map((col) => `${this._escapeHtml(col.title || col.dataIndx)}`).join(""); + const body = rows.map((row) => ( + `${columns.map((col) => `${this._escapeHtml(row[col.dataIndx] ?? "")}`).join("")}` + )).join(""); + return new Blob([ + `${header}${body}
` + ], { type: "application/vnd.ms-excel;charset=utf-8" }); + } + const csv = [ + columns.map((col) => this._csvCell(col.title || col.dataIndx)).join(","), + ...rows.map((row) => columns.map((col) => this._csvCell(row[col.dataIndx])).join(",")), + ].join("\n"); + return new Blob([csv], { type: "text/csv;charset=utf-8" }); + }, + + _csvCell(value) { + return `"${String(value ?? "").replaceAll('"', '""')}"`; + }, + + _escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + }, + + _saveBlob(content, filename) { + const blob = typeof content === "string" ? new Blob([content]) : content; + if (window.saveAs) { + window.saveAs(blob, filename); + return; + } + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + }, + + _getGridInstance() { + if (!this.gridRef.el || !window.$) { + return null; + } + try { + return $(this.gridRef.el).pqGrid("instance"); + } catch { + return null; + } + }, +}); diff --git a/addons_extensions/hr_payroll_extended/views/hr_payslip_employees_views.xml b/addons_extensions/hr_payroll_extended/views/hr_payslip_employees_views.xml new file mode 100644 index 000000000..a2f3dd83d --- /dev/null +++ b/addons_extensions/hr_payroll_extended/views/hr_payslip_employees_views.xml @@ -0,0 +1,91 @@ + + + + hr.payslip.employees.form.inherit.conflict.work.entries + hr.payslip.employees + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +