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,6 +1,6 @@
/** @odoo-module **/ /** @odoo-module **/
import {standardWidgetProps} from "@web/views/widgets/standard_widget_props"; import {standardWidgetProps} from "@web/views/widgets/standard_widget_props";
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; import {Component, onMounted, useRef, useState, onWillStart, onWillUpdateProps} from "@odoo/owl";
import {registry} from "@web/core/registry"; import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks"; import {useService} from "@web/core/utils/hooks";
import {loadJS, loadCSS} from "@web/core/assets"; import {loadJS, loadCSS} from "@web/core/assets";
@ -34,6 +34,12 @@ export class ConsolidatedPayslipGrid extends Component {
console.error("Grid element not found"); console.error("Grid element not found");
} }
}); });
onWillUpdateProps(async (nextProps) => {
if (nextProps.record.data.state !== this.props.record.data.state) {
await this.loadGrid();
}
});
} }
async loadDependencies() { async loadDependencies() {
@ -167,18 +173,6 @@ export class ConsolidatedPayslipGrid extends Component {
label: 'Refresh', label: 'Refresh',
icon: 'ui-icon-refresh', icon: 'ui-icon-refresh',
listener: () => this.loadGrid() 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', type: 'select',
@ -207,6 +201,7 @@ export class ConsolidatedPayslipGrid extends Component {
] ]
}, },
}; };
function updateData(data) { function updateData(data) {
$.ajax({ $.ajax({
url: "/slip/update", url: "/slip/update",
@ -295,7 +290,7 @@ export class ConsolidatedPayslipGrid extends Component {
async getColumns() { async getColumns() {
const subCols = await this.getSubgridColumns(); const subCols = await this.getSubgridColumns();
return [ const columns = [
{ {
title: "Employee", title: "Employee",
dataIndx: "employee", dataIndx: "employee",
@ -460,11 +455,18 @@ export class ConsolidatedPayslipGrid extends Component {
} }
}, },
];
if (this.props.record.data.state !== "paid") {
columns.push(
{ {
title: "Edit", title: "Edit",
width: 120, width: 120,
editable: false, editable: false,
render: function (ui) { render: function (ui) {
if (ui.rowData.state == 'paid') {
return ""
}
return "<button class='row-btn-edit' type='button'>Edit</button>" return "<button class='row-btn-edit' type='button'>Edit</button>"
}, },
postRender: function (ui) { postRender: function (ui) {
@ -479,7 +481,10 @@ export class ConsolidatedPayslipGrid extends Component {
}); });
} }
} }
]; )
}
return columns
} }
async getSubgridColumns() { async getSubgridColumns() {

View File

@ -122,7 +122,6 @@
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Gross Salary</strong></td> <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> <td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % income"/></strong></td>
</tr> </tr>
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Total Deduction</strong></td> <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"> <page name="salary_rules" string="Salary Rules">
<field name="rule_ids" readonly="not id"> <field name="rule_ids" readonly="not id">
<list> <list>
<field name="sequence" widget="handle"/>
<field name="name"/> <field name="name"/>
<field name="code"/> <field name="code"/>
<field name="category_id"/> <field name="category_id"/>
<field name="sequence" column_invisible="True"/>
<field name="partner_id"/> <field name="partner_id"/>
</list> </list>
</field> </field>

View File

@ -5,14 +5,21 @@
'installable': True, 'installable': True,
'application': True, 'application': True,
'depends': [ 'depends': [
'hr_payroll' 'hr_payroll',
'consolidated_batch_payslip',
'hr_work_entry_holidays'
], ],
'data': [ 'data': [
'data/hr_salary_advance_sequence.xml', 'data/hr_salary_advance_sequence.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/hr_payslip_employees_views.xml',
'views/hr_salary_advance_views.xml', 'views/hr_salary_advance_views.xml',
'views/menus.xml' 'views/menus.xml',
'views/hr_work_entry_views.xml',
], ],
'assets': { '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_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>