payroll changes
This commit is contained in:
parent
625bd67064
commit
5e617d3ff8
|
|
@ -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 `<span class="badge badge-${state === 'done' ? 'success' : 'warning'}">${state}</span>`;
|
||||
}
|
||||
},
|
||||
...subCols,
|
||||
{
|
||||
title: "View",
|
||||
width: 120,
|
||||
editable: false,
|
||||
summary:false,
|
||||
render: function (ui) {
|
||||
return "<button class='row-btn-view' type='button' >View</button>"
|
||||
},
|
||||
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 "<button class='row-btn-view' type='button' >View</button>"
|
||||
},
|
||||
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 "<button class='row-btn-edit' type='button'>Edit</button>"
|
||||
},
|
||||
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 "<button class='row-btn-edit' type='button'>Edit</button>"
|
||||
},
|
||||
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'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@
|
|||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Gross Salary</strong></td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % income"/></strong></td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Total Deduction</strong></td>
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@
|
|||
<page name="salary_rules" string="Salary Rules">
|
||||
<field name="rule_ids" readonly="not id">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="category_id"/>
|
||||
<field name="sequence" column_invisible="True"/>
|
||||
<field name="partner_id"/>
|
||||
</list>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
from . import hr_work_entry
|
||||
from . import hr_salary_advance
|
||||
from . import hr_payslip_employees
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) => `<th>${this._escapeHtml(col.title || col.dataIndx)}</th>`).join("");
|
||||
const body = rows.map((row) => (
|
||||
`<tr>${columns.map((col) => `<td>${this._escapeHtml(row[col.dataIndx] ?? "")}</td>`).join("")}</tr>`
|
||||
)).join("");
|
||||
return new Blob([
|
||||
`<table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`
|
||||
], { 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_payslip_by_employees_inherit_conflicts" model="ir.ui.view">
|
||||
<field name="name">hr.payslip.employees.form.inherit.conflict.work.entries</field>
|
||||
<field name="model">hr.payslip.employees</field>
|
||||
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_by_employees"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form/field[@name='employee_ids']" position="replace">
|
||||
<notebook>
|
||||
<page string="Employees">
|
||||
<field name="employee_ids" nolabel="1" widget="employee_line_many2many" context="{'list_view_ref' : 'hr_payroll.hr_payroll_employee_tree_inherit'}">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="work_email"/>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="structure_type_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Work Entry Conflicts">
|
||||
<group>
|
||||
<field name="conflict_work_entry_count" readonly="1"/>
|
||||
<!-- <button name="action_refresh_conflict_work_entries" type="object" string="Refresh Conflicts" class="btn-secondary"/>-->
|
||||
</group>
|
||||
<field name="conflict_work_entry_ids" nolabel="1" readonly="1" context="{'default_state': 'draft'}">
|
||||
<list editable="bottom" multi_edit="1" create="0" delete="0" edit="0">
|
||||
<field name="employee_id" readonly="1"/>
|
||||
<field name="name" string="Description"/>
|
||||
<field name="work_entry_type_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_stop"/>
|
||||
<field name="duration" widget="float_time" readonly="1"/>
|
||||
<field name="state" readonly="1"/>
|
||||
<button name="action_open_conflict"
|
||||
type="object"
|
||||
string="Open"
|
||||
icon="fa-external-link"/>
|
||||
<!-- <button name="action_validate" type="object" string="Validate" class="btn-primary"/>-->
|
||||
</list>
|
||||
<form string="Work Entry Conflict">
|
||||
<header>
|
||||
<button name="action_validate" type="object" string="Validate" class="btn-primary"/>
|
||||
<field name="state" widget="statusbar" readonly="1" statusbar_visible="draft,validated,conflict"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" string="Description"/>
|
||||
<field name="employee_id" readonly="1" widget="many2one_avatar_user"/>
|
||||
<field name="work_entry_type_id" options="{'no_create': True, 'no_open': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_start"/>
|
||||
<field name="date_stop"/>
|
||||
<field name="duration" widget="float_time" readonly="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Time Off Defer">
|
||||
<field name="timeoff_differ_ids" nolabel="1" readonly="1">
|
||||
<list editable="bottom" multi_edit="1" create="0" delete="0" edit="0">
|
||||
<field name="employee_id" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" on_change="1"/>
|
||||
<field name="department_id" optional="hidden" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
|
||||
<field name="holiday_status_id" class="fw-bold" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" on_change="1"/>
|
||||
<field name="name" optional="hidden"/>
|
||||
<field name="date_from" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" on_change="1"/>
|
||||
<field name="date_to" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" on_change="1"/>
|
||||
<field name="submitted_date" readonly="1" force_save="1" optional="hidden"/>
|
||||
<field name="duration_display" string="Duration"/>
|
||||
<field name="state" widget="badge" decoration-warning="state in ('confirm','validate1')" decoration-success="state == 'validate'" decoration-danger="state == 'cancel'"/>
|
||||
<field name="payslip_state" widget="state_selection" options="{'hide_label': False}"/>
|
||||
<field name="active_employee" column_invisible="True"/>
|
||||
<field name="user_id" column_invisible="True"/>
|
||||
<field name="message_needaction" column_invisible="True"/>
|
||||
<!-- <button string="Approve" name="action_approve" type="object" icon="fa-thumbs-up" invisible="state != 'confirm'"/>-->
|
||||
<field name="company_id" optional="hidden" column_invisible="True"/>
|
||||
<!-- <button name="action_validate" type="object" string="Validate" class="btn-primary"/>-->
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
</notebook>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payroll_hr_work_entry_form_view_inherit" model="ir.ui.view">
|
||||
<field name="name">payroll.hr.work.entry.view.form.inherit</field>
|
||||
<field name="model">hr.work.entry</field>
|
||||
<field name="inherit_id" ref="hr_work_entry.hr_work_entry_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='state']" position="before">
|
||||
<field name="leave_state" invisible="1"/>
|
||||
<button string="Refuse Time Off" name="action_refuse_leave" type="object" invisible="state != 'conflict' or not leave_id"/>
|
||||
<button string="Approve Time Off" name="action_approve_leave" type="object" invisible="state != 'conflict' or not leave_id or leave_state == 'validate'"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='work_entry_type_id']" position="after">
|
||||
<field name="leave_id" invisible="not leave_id"/>
|
||||
<field name="contract_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="payroll_hr_work_entry_form_view_inherit_contract" model="ir.ui.view">
|
||||
<field name="name">payroll.hr.work.entry.view.form.inherit.contract</field>
|
||||
<field name="model">hr.work.entry</field>
|
||||
<field name="inherit_id" ref="hr_work_entry_contract.hr_work_entry_contract_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='work_entry_undefined']" position="after">
|
||||
<div invisible="not work_entry_type_id">
|
||||
<div class="alert alert-warning" role="alert" invisible="not leave_id">
|
||||
This work entry cannot be validated. There is a leave to approve (or refuse) at the same time.
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert" invisible="leave_id">
|
||||
This work entry cannot be validated. It is conflicting with at least one work entry. <br/>
|
||||
Two work entries of the same employee cannot overlap at the same time.
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="payroll_leave_hr_work_entry_type_form_view_inherit" model="ir.ui.view">
|
||||
<field name="name">payroll.leave.hr.work.entry.type.view.form.inherit</field>
|
||||
<field name="model">hr.work.entry.type</field>
|
||||
<field name="inherit_id" ref="hr_work_entry.hr_work_entry_type_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="time_off" position="inside">
|
||||
<field name="leave_type_ids" widget="many2many_tags" invisible="not is_leave"/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue