diff --git a/addons_extensions/consolidated_batch_payslip/__init__.py b/addons_extensions/consolidated_batch_payslip/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons_extensions/consolidated_batch_payslip/__manifest__.py b/addons_extensions/consolidated_batch_payslip/__manifest__.py
new file mode 100644
index 000000000..eff682273
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/__manifest__.py
@@ -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,
+}
diff --git a/addons_extensions/consolidated_batch_payslip/models/__init__.py b/addons_extensions/consolidated_batch_payslip/models/__init__.py
new file mode 100644
index 000000000..86c0c139f
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/models/__init__.py
@@ -0,0 +1 @@
+from . import batch_payslip
\ No newline at end of file
diff --git a/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py b/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py
new file mode 100644
index 000000000..762af2245
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/models/batch_payslip.py
@@ -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]
\ No newline at end of file
diff --git a/addons_extensions/consolidated_batch_payslip/security/ir.model.access.csv b/addons_extensions/consolidated_batch_payslip/security/ir.model.access.csv
new file mode 100644
index 000000000..206887958
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/security/ir.model.access.csv
@@ -0,0 +1 @@
+id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js
new file mode 100644
index 000000000..963eb098d
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js
@@ -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 `${state}`;
+ }
+ },
+ ...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);
\ No newline at end of file
diff --git a/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml
new file mode 100644
index 000000000..df3dd93c7
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/addons_extensions/consolidated_batch_payslip/views/batch_payslip_view.xml b/addons_extensions/consolidated_batch_payslip/views/batch_payslip_view.xml
new file mode 100644
index 000000000..944bf480f
--- /dev/null
+++ b/addons_extensions/consolidated_batch_payslip/views/batch_payslip_view.xml
@@ -0,0 +1,18 @@
+
+
+ hr.payslip.run.form.inherit.consolidated.pqgrid.owl
+ hr.payslip.run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
index 5035f686a..aeab3e515 100644
--- a/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
+++ b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py
@@ -237,7 +237,7 @@ class BiometricDeviceDetails(models.Model):
if fingerprint:
fingerprint.finger_template = base64_data
else:
- self.env['hr.employee.fingerprint'].create({
+ self.env['fingerprint.templates'].create({
'finger_template': base64_data,
'finger_id': finger.fid,
'employee_bio_id': employee.id,