commit
c222a2c0d2
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
'name': 'Consolidated Payslip Grid (OWL)',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Human Resources',
|
||||||
|
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
|
||||||
|
'author': 'Raman Marikanti',
|
||||||
|
'depends': ['hr_payroll', 'web'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/batch_payslip_view.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
# Internal module JS and XML files (ensure correct paths within 'static/src')
|
||||||
|
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js',
|
||||||
|
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import batch_payslip
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
class HrPayslipRun(models.Model):
|
||||||
|
_inherit = 'hr.payslip.run'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_consolidated_attendance_data(self, payslip_run_id):
|
||||||
|
"""
|
||||||
|
Returns consolidated attendance and leave data for all employees in the payslip run
|
||||||
|
"""
|
||||||
|
# Get all payslips in this batch
|
||||||
|
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
||||||
|
|
||||||
|
if not payslips:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for slip in payslips:
|
||||||
|
employee = slip.employee_id
|
||||||
|
contract = slip.contract_id
|
||||||
|
|
||||||
|
# Get attendance data
|
||||||
|
attendance_days = self._get_attendance_days(slip)
|
||||||
|
worked_days = self._get_worked_days(slip)
|
||||||
|
leave_days = self._get_leave_days(slip)
|
||||||
|
lop_days = self._get_lop_days(slip)
|
||||||
|
|
||||||
|
# Get leave balances
|
||||||
|
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
|
||||||
|
leave_taken = self._get_leave_taken(slip)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'id': slip.id,
|
||||||
|
'employee_id': (employee.id, employee.name),
|
||||||
|
'employee_code': employee.employee_id or '',
|
||||||
|
'department_id': (employee.department_id.id,
|
||||||
|
employee.department_id.name) if employee.department_id else False,
|
||||||
|
'total_days': (slip.date_to - slip.date_from).days + 1,
|
||||||
|
'worked_days': worked_days,
|
||||||
|
'attendance_days': attendance_days,
|
||||||
|
'leave_days': leave_days,
|
||||||
|
'lop_days': lop_days,
|
||||||
|
'doj':employee.doj,
|
||||||
|
'birthday':employee.birthday,
|
||||||
|
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
|
||||||
|
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
|
||||||
|
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
|
||||||
|
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
|
||||||
|
'sick_leave_taken': leave_taken.get('sick', 0),
|
||||||
|
'casual_leave_taken': leave_taken.get('casual', 0),
|
||||||
|
'privilege_leave_taken': leave_taken.get('privilege', 0),
|
||||||
|
'state': slip.state,
|
||||||
|
'lines':self.get_payslip_lines_data(slip),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def sub_columns(self,payslip_run_id):
|
||||||
|
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
||||||
|
|
||||||
|
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
|
||||||
|
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
|
||||||
|
|
||||||
|
columns = []
|
||||||
|
for code, name in code_name_dict.items():
|
||||||
|
columns.append({
|
||||||
|
'title': name,
|
||||||
|
'dataIndx': code,
|
||||||
|
'width': 150,
|
||||||
|
'editable': False,
|
||||||
|
'summary': {'type': "sum_"},
|
||||||
|
})
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def save_consolidated_attendance_data(self, payslip_run_id, data):
|
||||||
|
"""
|
||||||
|
Saves the edited attendance and leave data from the grid
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
slip = self.env['hr.payslip'].browse(item['id'])
|
||||||
|
if slip.state != 'draft':
|
||||||
|
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
|
||||||
|
|
||||||
|
# Update LOP days
|
||||||
|
if 'lop_days' in item:
|
||||||
|
self._update_lop_days(slip, float(item['lop_days']))
|
||||||
|
|
||||||
|
# Update leave days taken
|
||||||
|
leave_updates = {}
|
||||||
|
if 'sick_leave_taken' in item:
|
||||||
|
leave_updates['sick'] = float(item['sick_leave_taken'])
|
||||||
|
if 'casual_leave_taken' in item:
|
||||||
|
leave_updates['casual'] = float(item['casual_leave_taken'])
|
||||||
|
if 'privilege_leave_taken' in item:
|
||||||
|
leave_updates['privilege'] = float(item['privilege_leave_taken'])
|
||||||
|
|
||||||
|
if leave_updates:
|
||||||
|
self._update_leave_taken(slip, leave_updates)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def recalculate_lop_days(self, payslip_run_id):
|
||||||
|
"""
|
||||||
|
Recalculates LOP days for all payslips in the batch based on attendance
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
payslips = self.env['hr.payslip'].search([
|
||||||
|
('payslip_run_id', '=', payslip_run_id),
|
||||||
|
('state', '=', 'draft')
|
||||||
|
])
|
||||||
|
|
||||||
|
for slip in payslips:
|
||||||
|
attendance_days = self._get_attendance_days(slip)
|
||||||
|
expected_days = (slip.date_to - slip.date_from).days + 1
|
||||||
|
lop_days = expected_days - attendance_days
|
||||||
|
self._update_lop_days(slip, lop_days)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_all_attendance_data(self, payslip_run_id):
|
||||||
|
"""
|
||||||
|
Marks all payslips in the batch as validated
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
payslips = self.env['hr.payslip'].search([
|
||||||
|
('payslip_run_id', '=', payslip_run_id),
|
||||||
|
('state', '=', 'draft')
|
||||||
|
])
|
||||||
|
|
||||||
|
if not payslips:
|
||||||
|
raise UserError(_("No draft payslips found in this batch"))
|
||||||
|
|
||||||
|
payslips.write({'state': 'verify'})
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
def _get_attendance_days(self, payslip):
|
||||||
|
"""
|
||||||
|
Returns number of days employee was present (based on attendance records)
|
||||||
|
"""
|
||||||
|
attendance_records = self.env['hr.attendance'].search([
|
||||||
|
('employee_id', '=', payslip.employee_id.id),
|
||||||
|
('check_in', '>=', payslip.date_from),
|
||||||
|
('check_in', '<=', payslip.date_to)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Group by day
|
||||||
|
unique_days = set()
|
||||||
|
for att in attendance_records:
|
||||||
|
unique_days.add(att.check_in.date())
|
||||||
|
|
||||||
|
return len(unique_days)
|
||||||
|
|
||||||
|
def _get_worked_days(self, payslip):
|
||||||
|
"""
|
||||||
|
Returns number of working days (excluding weekends and holidays)
|
||||||
|
"""
|
||||||
|
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
|
||||||
|
|
||||||
|
def get_payslip_lines_data(self, payslip_id):
|
||||||
|
list = []
|
||||||
|
for line in payslip_id.line_ids:
|
||||||
|
list.append({
|
||||||
|
'name': line.name,
|
||||||
|
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
|
||||||
|
'category_id': line.category_id.name if line.category_id else False,
|
||||||
|
'amount': line.amount,
|
||||||
|
'quantity': line.quantity,
|
||||||
|
'rate': line.rate
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
|
||||||
|
def _get_leave_days(self, payslip):
|
||||||
|
"""
|
||||||
|
Returns total leave days taken in this period
|
||||||
|
"""
|
||||||
|
leave_lines = payslip.worked_days_line_ids.filtered(
|
||||||
|
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
|
||||||
|
)
|
||||||
|
return sum(leave_lines.mapped('number_of_days'))
|
||||||
|
|
||||||
|
def _get_lop_days(self, payslip):
|
||||||
|
"""
|
||||||
|
Returns LOP days from payslip
|
||||||
|
"""
|
||||||
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
|
||||||
|
return lop_line.number_of_days if lop_line else 0
|
||||||
|
|
||||||
|
|
||||||
|
def _get_leave_taken(self, payslip):
|
||||||
|
"""
|
||||||
|
Returns leave days taken in this payslip period
|
||||||
|
"""
|
||||||
|
leave_lines = payslip.worked_days_line_ids
|
||||||
|
return {
|
||||||
|
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
|
||||||
|
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
|
||||||
|
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_lop_days(self, payslip, days):
|
||||||
|
"""
|
||||||
|
Updates LOP days in the payslip
|
||||||
|
"""
|
||||||
|
WorkedDays = self.env['hr.payslip.worked_days']
|
||||||
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
|
||||||
|
|
||||||
|
if lop_line:
|
||||||
|
if days > 0:
|
||||||
|
lop_line.write({'number_of_days': days})
|
||||||
|
else:
|
||||||
|
lop_line.unlink()
|
||||||
|
elif days > 0:
|
||||||
|
WorkedDays.create({
|
||||||
|
'payslip_id': payslip.id,
|
||||||
|
'name': _('Loss of Pay'),
|
||||||
|
'code': 'LOP',
|
||||||
|
'number_of_days': days,
|
||||||
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
||||||
|
'contract_id': payslip.contract_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _get_leave_balances(self, employee, date_from, date_to):
|
||||||
|
Leave = self.env['hr.leave']
|
||||||
|
Allocation = self.env['hr.leave.allocation']
|
||||||
|
leave_types = self.env['hr.leave.type'].search([])
|
||||||
|
|
||||||
|
balances = {}
|
||||||
|
for leave_type in leave_types:
|
||||||
|
# Approved allocations within or before payslip period
|
||||||
|
allocations = Allocation.search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
('holiday_status_id', '=', leave_type.id),
|
||||||
|
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
|
||||||
|
])
|
||||||
|
allocated = sum(alloc.number_of_days for alloc in allocations)
|
||||||
|
|
||||||
|
# Approved leaves within the payslip period
|
||||||
|
leaves = Leave.search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
('holiday_status_id', '=', leave_type.id),
|
||||||
|
('request_date_to', '<=', str(date_to))
|
||||||
|
])
|
||||||
|
used = sum(leave.number_of_days for leave in leaves)
|
||||||
|
|
||||||
|
# Key: leave code or fallback to name
|
||||||
|
code = leave_type.work_entry_type_id.code
|
||||||
|
balances[code] = allocated - used
|
||||||
|
|
||||||
|
return balances
|
||||||
|
|
||||||
|
def _update_leave_taken(self, payslip, leave_data):
|
||||||
|
"""
|
||||||
|
Updates leave days taken in the payslip
|
||||||
|
"""
|
||||||
|
WorkedDays = self.env['hr.payslip.worked_days']
|
||||||
|
|
||||||
|
for leave_type, days in leave_data.items():
|
||||||
|
code = leave_type.upper()
|
||||||
|
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
|
||||||
|
|
||||||
|
|
||||||
|
if line:
|
||||||
|
if days > 0:
|
||||||
|
line.write({'number_of_days': days})
|
||||||
|
else:
|
||||||
|
line.unlink()
|
||||||
|
elif days > 0:
|
||||||
|
WorkedDays.create({
|
||||||
|
'payslip_id': payslip.id,
|
||||||
|
'name': _(leave_type.capitalize() + ' Leave'),
|
||||||
|
'code': code,
|
||||||
|
'number_of_days': days,
|
||||||
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
||||||
|
'contract_id': payslip.contract_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
def get_payslip_lines_data(self, payslip_id):
|
||||||
|
payslip = self.browse(payslip_id)
|
||||||
|
return [{
|
||||||
|
'name': line.name,
|
||||||
|
'code': line.code,
|
||||||
|
'category_id': (line.category_id.id, line.category_id.name),
|
||||||
|
'amount': line.amount,
|
||||||
|
'quantity': line.quantity,
|
||||||
|
'rate': line.rate
|
||||||
|
} for line in payslip.line_ids]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
|
|
@ -0,0 +1,508 @@
|
||||||
|
/** @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";
|
||||||
|
|
||||||
|
export class ConsolidatedPayslipGrid extends Component {
|
||||||
|
static props = {
|
||||||
|
...standardWidgetProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
static template = "ConsolidatedPayslipGrid";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.gridRef = useRef("gridContainer");
|
||||||
|
this.state = useState({
|
||||||
|
rows: [],
|
||||||
|
payslipRunId: this.props.record.resId || this.props.record.evalContext.id || false
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
await this.loadDependencies();
|
||||||
|
await this.loadGrid();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (this.gridRef.el) {
|
||||||
|
this.renderGrid();
|
||||||
|
} else {
|
||||||
|
console.error("Grid element not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDependencies() {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"),
|
||||||
|
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"),
|
||||||
|
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css"),
|
||||||
|
loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js"),
|
||||||
|
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jszip/2.6.1/jszip.min.js"),
|
||||||
|
loadJS("https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"),
|
||||||
|
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.css"),
|
||||||
|
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/themes/Office/pqgrid.min.css")
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set global jQuery references
|
||||||
|
window.$ = window.jQuery = window.$ || window.jQuery;
|
||||||
|
|
||||||
|
// Verify pqGrid is loaded
|
||||||
|
if (!window.pq) {
|
||||||
|
throw new Error("pqGrid failed to load");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load dependencies:", error);
|
||||||
|
this.showNotification("Failed to load required components", "danger");
|
||||||
|
throw error; // Re-throw to prevent further execution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGrid() {
|
||||||
|
try {
|
||||||
|
if (!this.state.payslipRunId) {
|
||||||
|
console.error("No payslip run ID found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await this.orm.call("hr.payslip.run", "get_consolidated_attendance_data", [this.state.payslipRunId]);
|
||||||
|
|
||||||
|
|
||||||
|
const data = records.map(rec => {
|
||||||
|
// Base object
|
||||||
|
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 || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add each line.code: line.amount into the row
|
||||||
|
for (const line of rec.lines || []) {
|
||||||
|
if (line.code) {
|
||||||
|
row[line.code] = line.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.state.rows = data;
|
||||||
|
this.renderGrid();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderGrid() {
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
saveAs(blob, "PaySheet."+ format );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply CSS and initialize grid
|
||||||
|
$(this.gridRef.el)
|
||||||
|
.css({ height: '600px', width: '100%' })
|
||||||
|
.pqGrid(gridOptions)
|
||||||
|
.pqGrid("refreshDataAndView");
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshDataAndView() {
|
||||||
|
try {
|
||||||
|
await this.loadGrid();
|
||||||
|
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to refresh grid data:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChanges() {
|
||||||
|
const grid = $(this.gridRef.el).pqGrid();
|
||||||
|
const updatedData = grid.pqGrid("option", "dataModel.data");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.orm.call(
|
||||||
|
"hr.payslip.run",
|
||||||
|
"save_consolidated_attendance_data",
|
||||||
|
[this.state.payslipRunId, updatedData]
|
||||||
|
);
|
||||||
|
await this.refreshDataAndView();
|
||||||
|
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.refreshDataAndView();
|
||||||
|
this.showNotification("LOP days recalculated successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error recalculating LOP:", error);
|
||||||
|
this.showNotification("Error recalculating LOP!", "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAll() {
|
||||||
|
try {
|
||||||
|
await this.orm.call(
|
||||||
|
"hr.payslip.run",
|
||||||
|
"validate_all_attendance_data",
|
||||||
|
[this.state.payslipRunId]
|
||||||
|
);
|
||||||
|
await this.refreshDataAndView();
|
||||||
|
this.showNotification("All records validated successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error validating records:", error);
|
||||||
|
this.showNotification("Error validating records!", "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = "success") {
|
||||||
|
// Implement notification display (could use Odoo's notification service)
|
||||||
|
console.log(`${type}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getColumns() {
|
||||||
|
const subCols = await this.getSubgridColumns();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Employee",
|
||||||
|
dataIndx: "employee",
|
||||||
|
width: 200,
|
||||||
|
filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] },
|
||||||
|
editable: false,
|
||||||
|
menuIcon: true,
|
||||||
|
menuInHide: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Employee ID",
|
||||||
|
dataIndx: "employee_code",
|
||||||
|
width: 200,
|
||||||
|
filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] },
|
||||||
|
editable: false,
|
||||||
|
menuInHide: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Department",
|
||||||
|
dataIndx: "department",
|
||||||
|
width: 150,
|
||||||
|
filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] },
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Date Of Joining",
|
||||||
|
dataIndx: "doj",
|
||||||
|
width: 100,
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Date Of Brith",
|
||||||
|
dataIndx: "birthday",
|
||||||
|
width: 100,
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bank",
|
||||||
|
dataIndx: "bank",
|
||||||
|
width: 150,
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Total Days",
|
||||||
|
dataIndx: "total_days",
|
||||||
|
width: 80,
|
||||||
|
dataType: "integer",
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Worked Days",
|
||||||
|
dataIndx: "worked_days",
|
||||||
|
width: 90,
|
||||||
|
dataType: "integer",
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Attendance Days",
|
||||||
|
// dataIndx: "attendance_days",
|
||||||
|
// width: 100,
|
||||||
|
// dataType: "integer",
|
||||||
|
// editable: false
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: "Leave Days",
|
||||||
|
dataIndx: "leave_days",
|
||||||
|
width: 80,
|
||||||
|
dataType: "integer",
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LOP Days",
|
||||||
|
dataIndx: "lop_days",
|
||||||
|
width: 80,
|
||||||
|
dataType: "integer",
|
||||||
|
editable: (rowData) => rowData.state === 'draft',
|
||||||
|
summary: { type: "sum_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Sick Leave Balance",
|
||||||
|
dataIndx: "sick_leave_balance",
|
||||||
|
width: 100,
|
||||||
|
dataType: "float",
|
||||||
|
editable: false,
|
||||||
|
summary: { type: "sum_" },
|
||||||
|
format: "##,##0.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Casual Leave Balance",
|
||||||
|
dataIndx: "casual_leave_balance",
|
||||||
|
width: 100,
|
||||||
|
dataType: "float",
|
||||||
|
editable: false,
|
||||||
|
summary: { type: "sum_" },
|
||||||
|
format: "##,##0.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Privilege Leave Balance",
|
||||||
|
dataIndx: "privilege_leave_balance",
|
||||||
|
width: 100,
|
||||||
|
dataType: "float",
|
||||||
|
editable: false,
|
||||||
|
summary: { type: "sum_" },
|
||||||
|
format: "##,##0.00"
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Sick Leave Taken",
|
||||||
|
// dataIndx: "sick_leave_taken",
|
||||||
|
// width: 100,
|
||||||
|
// dataType: "float",
|
||||||
|
// editable: (rowData) => rowData.state === 'draft',
|
||||||
|
// summary: { type: "sum_" },
|
||||||
|
// format: "##,##0.00"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Casual Leave Taken",
|
||||||
|
// dataIndx: "casual_leave_taken",
|
||||||
|
// width: 100,
|
||||||
|
// dataType: "float",
|
||||||
|
// editable: (rowData) => rowData.state === 'draft',
|
||||||
|
// summary: { type: "sum_" },
|
||||||
|
// format: "##,##0.00"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Privilege Leave Taken",
|
||||||
|
// dataIndx: "privilege_leave_taken",
|
||||||
|
// width: 100,
|
||||||
|
// dataType: "float",
|
||||||
|
// editable: (rowData) => rowData.state === 'draft',
|
||||||
|
// summary: { type: "sum_" },
|
||||||
|
// format: "##,##0.00"
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: "Status",
|
||||||
|
dataIndx: "state",
|
||||||
|
width: 80,
|
||||||
|
editable: false,
|
||||||
|
render: (ui) => {
|
||||||
|
const state = ui.rowData.state;
|
||||||
|
return `<span class="badge badge-${state === 'done' ? 'success' : 'warning'}">${state}</span>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...subCols
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSubgridColumns() {
|
||||||
|
const response = await this.orm.call(
|
||||||
|
"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' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing grid:", error);
|
||||||
|
this.notification.add("Error refreshing data", { type: 'danger' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChanges() {
|
||||||
|
const grid = $(this.gridRef.el).pqGrid("instance");
|
||||||
|
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.notification.add("Changes saved successfully", { type: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving data:", error);
|
||||||
|
this.notification.add("Error saving changes", { type: 'danger' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async recalculateLOP() {
|
||||||
|
try {
|
||||||
|
await this.orm.call(
|
||||||
|
"hr.payslip.run",
|
||||||
|
"recalculate_lop_days",
|
||||||
|
[this.state.payslipRunId]
|
||||||
|
);
|
||||||
|
await this.refreshGrid();
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAll() {
|
||||||
|
try {
|
||||||
|
await this.orm.call(
|
||||||
|
"hr.payslip.run",
|
||||||
|
"validate_all_attendance_data",
|
||||||
|
[this.state.payslipRunId]
|
||||||
|
);
|
||||||
|
await this.refreshGrid();
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const consolidatedPayslipGrid = {
|
||||||
|
component: ConsolidatedPayslipGrid,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("view_widgets").add("ConsolidatedPayslipGrid", consolidatedPayslipGrid);
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="ConsolidatedPayslipGrid" owl="1">
|
||||||
|
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
|
||||||
|
<field name="model">hr.payslip.run</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//sheet" position="inside">
|
||||||
|
<notebook>
|
||||||
|
<page string="Consolidated Payslip">
|
||||||
|
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
|
||||||
|
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
|
||||||
|
</div>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -237,7 +237,7 @@ class BiometricDeviceDetails(models.Model):
|
||||||
if fingerprint:
|
if fingerprint:
|
||||||
fingerprint.finger_template = base64_data
|
fingerprint.finger_template = base64_data
|
||||||
else:
|
else:
|
||||||
self.env['hr.employee.fingerprint'].create({
|
self.env['fingerprint.templates'].create({
|
||||||
'finger_template': base64_data,
|
'finger_template': base64_data,
|
||||||
'finger_id': finger.fid,
|
'finger_id': finger.fid,
|
||||||
'employee_bio_id': employee.id,
|
'employee_bio_id': employee.id,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue