payroll changes

This commit is contained in:
pranaysaidurga 2026-07-01 17:22:14 +05:30
parent 625bd67064
commit 5e617d3ff8
10 changed files with 639 additions and 169 deletions

View File

@ -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'});
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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',
],
},
}

View File

@ -1 +1,3 @@
from . import hr_work_entry
from . import hr_salary_advance
from . import hr_payslip_employees

View File

@ -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",
}

View File

@ -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
}

View File

@ -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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
},
_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;
}
},
});

View File

@ -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>

View File

@ -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>