diff --git a/addons_extensions/hr_attendance_extended/models/hr_attendance.py b/addons_extensions/hr_attendance_extended/models/hr_attendance.py index 196917d8b..5d24714fa 100644 --- a/addons_extensions/hr_attendance_extended/models/hr_attendance.py +++ b/addons_extensions/hr_attendance_extended/models/hr_attendance.py @@ -104,3 +104,10 @@ class AttendanceData(models.Model): # extra_hours = fields.Float() status = fields.Selection([('leave','On Leave'),('present','Present'),('no_info','No Information')]) attendance_id = fields.Many2one('attendance.attendance') + + + +class HRAttendnace(models.Model): + _inherit = 'hr.attendance' + + employee_id = fields.Many2one(group_expand='') diff --git a/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py index 90e9f98dc..7b5ae2ab6 100644 --- a/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py +++ b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py @@ -1,4 +1,4 @@ -from odoo import models, fields, api +from odoo import models, fields, api,_ from datetime import datetime, timedelta import xlwt from io import BytesIO @@ -13,15 +13,19 @@ class AttendanceReport(models.Model): _name = 'attendance.report' _description = 'Attendance Report' - @api.model - def get_attendance_report(self, employee_id, start_date, end_date): + def get_attendance_report(self, department_id, employee_id, start_date, end_date): # Ensure start_date and end_date are in the correct format (datetime) - if employee_id == '-': employee_id = False else: employee_id = int(employee_id) + + if department_id == '-': + department_id = False + else: + department_id = int(department_id) + if isinstance(start_date, str): start_date = datetime.strptime(start_date, '%d/%m/%Y') if isinstance(end_date, str): @@ -31,17 +35,59 @@ class AttendanceReport(models.Model): start_date_str = start_date.strftime('%Y-%m-%d') end_date_str = end_date.strftime('%Y-%m-%d') - # Define the base where condition - if employee_id: - case = """WHERE emp.id = %s AND at.check_in >= %s AND at.check_out <= %s""" - params = (employee_id, start_date_str, end_date_str) - else: - case = """WHERE at.check_in >= %s AND at.check_out <= %s""" - params = (start_date_str, end_date_str) + # Initialize parameters list with date range for date_range CTE + params = [start_date_str, end_date_str] - # Define the query with improved date handling + # Build conditions for employee_dates CTE + emp_date_conditions = [] + emp_date_params = [] + if employee_id: + emp_date_conditions.append("emp.id = %s") + emp_date_params.append(employee_id) + if department_id: + emp_date_conditions.append("emp.department_id = %s") + emp_date_params.append(department_id) + + # Build conditions for daily_checkins CTE + checkin_conditions = ["at.check_in >= %s", "at.check_out <= %s"] + checkin_params = [start_date_str, end_date_str] + if employee_id: + checkin_conditions.append("emp.id = %s") + checkin_params.append(employee_id) + if department_id: + checkin_conditions.append("emp.department_id = %s") + checkin_params.append(department_id) + + # Define the query query = """ - WITH daily_checkins AS ( + WITH date_range AS ( + SELECT generate_series( + %s::date, + %s::date, + interval '1 day' + )::date AS date + ), + employee_dates AS ( + SELECT + emp.id AS employee_id, + emp.name AS employee_name, + dr.date, + TO_CHAR(dr.date, 'Day') AS day_name, + EXTRACT(WEEK FROM dr.date) AS week_number, + TO_CHAR(date_trunc('week', dr.date), 'MON DD') || ' - ' || + TO_CHAR(date_trunc('week', dr.date) + interval '6 days', 'MON DD') AS week_range, + dep.name->>'en_US' AS department + FROM + hr_employee emp + CROSS JOIN + date_range dr + LEFT JOIN + hr_department dep ON emp.department_id = dep.id + WHERE + emp.active = true + """ + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """ + ), + daily_checkins AS ( SELECT emp.id, emp.name, @@ -50,97 +96,276 @@ class AttendanceReport(models.Model): at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out, at.worked_hours, ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row, - ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row + ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row, + dep.name->>'en_US' AS department FROM hr_attendance at LEFT JOIN hr_employee emp ON at.employee_id = emp.id - """ + case + """ + LEFT JOIN + hr_department dep ON emp.department_id = dep.id + WHERE + """ + " AND ".join(checkin_conditions) + """ + ), + attendance_summary AS ( + SELECT + id, + name, + date, + MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in, + MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out, + SUM(worked_hours) AS total_worked_hours, + department + FROM + daily_checkins + GROUP BY + id, name, date, department + ), + leave_data AS ( + SELECT + hl.employee_id, + hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start, + hl.date_to AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_end, + hlt.name->>'en_US' AS leave_type, + hl.request_unit_half AS is_half_day, + hl.request_date_from, + hl.request_date_to + FROM + hr_leave hl + JOIN + hr_leave_type hlt ON hl.holiday_status_id = hlt.id + WHERE + hl.state IN ('validate', 'confirm', 'validate1') + AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp) ) SELECT - id, - name, - date, - MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in, - MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out, - SUM(worked_hours) AS total_worked_hours + ed.employee_id AS id, + ed.employee_name AS name, + ed.date, + 'Week ' || ed.week_number || ' (' || ed.week_range || ')' AS week_info, + TRIM(ed.day_name) AS day_name, + COALESCE(ats.first_check_in, NULL) AS first_check_in, + COALESCE(ats.last_check_out, NULL) AS last_check_out, + COALESCE(ats.total_worked_hours, 0) AS total_worked_hours, + ed.department, + CASE + WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type + WHEN ld.leave_type IS NOT NULL THEN 'on ' || ld.leave_type + WHEN ats.first_check_in IS NOT NULL THEN 'Present' + ELSE 'NA' + END AS status FROM - daily_checkins - GROUP BY - id, name, date + employee_dates ed + LEFT JOIN + attendance_summary ats ON ed.employee_id = ats.id AND ed.date = ats.date + LEFT JOIN + leave_data ld ON ed.employee_id = ld.employee_id + AND ed.date >= DATE(ld.leave_start) + AND ed.date <= DATE(ld.leave_end) ORDER BY - id, date; + ed.employee_id, ed.date; """ - # Execute the query with parameters - self.env.cr.execute(query, params) - rows = self.env.cr.dictfetchall() - data = [] - a = 0 - for r in rows: - a += 1 - # Calculate worked hours in Python, but here it's better done in the query itself. - worked_hours = r['last_check_out'] - r['first_check_in'] if r['first_check_in'] and r[ - 'last_check_out'] else 0 + # Combine all parameters in the correct order: + # 1. date_range params (start_date_str, end_date_str) + # 2. employee_dates params (emp_date_params) + # 3. daily_checkins params (checkin_params) + # 4. leave_data params (start_date_str, end_date_str) + all_params = [ + start_date_str, end_date_str, # date_range + *emp_date_params, # employee_dates + *checkin_params, # daily_checkins + start_date_str, end_date_str # leave_data + ] - data.append({ - 'id': a, - 'employee_id': r['id'], - 'employee_name': r['name'], - 'date': r['date'], - 'check_in': r['first_check_in'], - 'check_out': r['last_check_out'], - 'worked_hours': worked_hours, - }) + try: + # Execute the query with parameters + self.env.cr.execute(query, all_params) + rows = self.env.cr.dictfetchall() + + data = [] + for idx, r in enumerate(rows, 1): + data.append({ + 'id': idx, + 'employee_id': r['id'], + 'employee_name': r['name'], + 'employee_department': r['department'], + 'date': r['date'], + 'week_info': r['week_info'], + 'day_name': r['day_name'], + 'check_in': r['first_check_in'], + 'check_out': r['last_check_out'], + 'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0, + 'status': r['status'] + }) + + return data + + except Exception as e: + error_msg = f"Error executing attendance report query: {str(e)}" + print(error_msg) + raise UserError( + _("An error occurred while generating the attendance report. Please check the logs for details.")) - return data @api.model - def export_to_excel(self, employee_id, start_date, end_date): - # Fetch the attendance data (replace with your logic to fetch attendance data) - attendance_data = self.get_attendance_report(employee_id, start_date, end_date) - + def export_to_excel(self, department_id, employee_id, start_date, end_date): + attendance_data = self.get_attendance_report(department_id, employee_id, start_date, end_date) if not attendance_data: raise UserError("No data to export!") - # Create an Excel workbook and a sheet - workbook = xlwt.Workbook() + # Create workbook and sheet + workbook = xlwt.Workbook(encoding='utf-8') sheet = workbook.add_sheet('Attendance Report') - # Define the column headers - headers = ['Employee Name', 'Check-in', 'Check-out', 'Worked Hours'] + # Define styles - using only xlwt supported color names + title_style = xlwt.easyxf( + 'font: bold on, height 300, color white;' + 'pattern: pattern solid, fore_color dark_blue;' + 'align: vert centre, horiz center;' + ) + + header_style = xlwt.easyxf( + 'font: bold on, height 240, color white;' + 'pattern: pattern solid, fore_color dark_blue;' # Changed from navy to dark_blue + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz center' + ) + + data_style = xlwt.easyxf( + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left' + ) + + time_style = xlwt.easyxf( + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left', + num_format_str='YYYY-MM-DD HH:MM:SS' + ) + + date_style = xlwt.easyxf( + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left', + num_format_str='YYYY-MM-DD' + ) + + hours_style = xlwt.easyxf( + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz right', + num_format_str='0.00' + ) + + # Status color styles using supported colors + status_present = xlwt.easyxf( + 'font: color green;' + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left' + ) + + status_leave = xlwt.easyxf( + 'font: color red;' + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left' + ) + + status_halfday = xlwt.easyxf( + 'font: color orange;' + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left' + ) + + status_na = xlwt.easyxf( + 'font: color blue;' + 'borders: left thin, right thin, top thin, bottom thin;' + 'align: vert centre, horiz left' + ) + + # Set column widths (in units of 1/256 of a character width) + col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 5000] + for i, width in enumerate(col_widths): + sheet.col(i).width = width + + # Write title + sheet.write_merge(0, 0, 0, 8, 'ATTENDANCE REPORT', title_style) + + # Write date range + date_range = f"From: {start_date} To: {end_date}" + sheet.write_merge(1, 1, 0, 8, date_range, xlwt.easyxf( + 'font: italic on; align: horiz center' + )) + + # Write headers + headers = [ + 'Department', 'Employee Name','Week', 'Date', 'Day', + 'Check-in', 'Check-out', 'Worked Hours', 'Status' + ] - # Write headers to the first row for col_num, header in enumerate(headers): - sheet.write(0, col_num, header) + sheet.write(2, col_num, header, header_style) - # Write the attendance data to the sheet - for row_num, record in enumerate(attendance_data, start=1): - sheet.write(row_num, 0, record['employee_name']) - sheet.write(row_num, 1, record['check_in'].strftime("%Y-%m-%d %H:%M:%S")) - sheet.write(row_num, 2, record['check_out'].strftime("%Y-%m-%d %H:%M:%S")) - if isinstance(record['worked_hours'], timedelta): - hours = record['worked_hours'].seconds // 3600 - minutes = (record['worked_hours'].seconds % 3600) // 60 - # Format as "X hours Y minutes" - worked_hours_str = f"{record['worked_hours'].days * 24 + hours} hours {minutes} minutes" - sheet.write(row_num, 3, worked_hours_str) + # Write data rows + current_employee = None + for row_num, record in enumerate(attendance_data, start=3): + # Highlight employee changes with a subtle border + if current_employee != record['employee_name']: + current_employee = record['employee_name'] + sheet.row(row_num).height = 400 # Slightly taller row for new employee + + # Apply appropriate status style + if 'Present' in record['status']: + status_style = status_present + elif 'Half day' in record['status']: + status_style = status_halfday + elif 'on ' in record['status']: + status_style = status_leave else: - sheet.write(row_num, 3, record['worked_hours']) - # Save the workbook to a BytesIO buffer + status_style = status_na + + # Write data + sheet.write(row_num, 0, record['employee_department'], data_style) + sheet.write(row_num, 1, record['employee_name'], data_style) + sheet.write(row_num, 2, record['week_info'], data_style) + sheet.write(row_num, 3, record['date'], date_style) + sheet.write(row_num, 4, record['day_name'], data_style) + + # Check-in/Check-out times + if record['check_in']: + sheet.write(row_num, 5, record['check_in'].strftime("%Y-%m-%d %H:%M:%S"), time_style) + else: + sheet.write(row_num, 5, '', data_style) + + if record['check_out']: + sheet.write(row_num, 6, record['check_out'].strftime("%Y-%m-%d %H:%M:%S"), time_style) + else: + sheet.write(row_num, 6, '', data_style) + + # Worked hours formatting + if isinstance(record['worked_hours'], (float, int)): + sheet.write(row_num, 7, float(record['worked_hours']), hours_style) + else: + sheet.write(row_num, 7, str(record['worked_hours']), data_style) + + sheet.write(row_num, 8, record['status'], status_style) + + # Add freeze panes (headers will stay visible when scrolling) + sheet.set_panes_frozen(True) + sheet.set_horz_split_pos(4) # After row 3 (headers) + sheet.set_vert_split_pos(0) # No vertical split + + # Save to buffer output = BytesIO() workbook.save(output) - - # Convert the output to base64 for saving in Odoo file_data = base64.b64encode(output.getvalue()) - # Create an attachment record to save the Excel file in Odoo + # Create attachment with timestamp + report_date = datetime.now().strftime("%Y-%m-%d_%H-%M") + filename = f"Attendance_Report_{report_date}.xls" + attachment = self.env['ir.attachment'].create({ - 'name': 'attendance_report.xls', + 'name': filename, 'type': 'binary', 'datas': file_data, 'mimetype': 'application/vnd.ms-excel', }) - # Return the attachment's URL to allow downloading in the Odoo UI - return '/web/content/%d/%s' % (attachment.id, attachment.name), + return '/web/content/%d/%s' % (attachment.id, attachment.name), \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js b/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js index be4eeb448..92faba293 100644 --- a/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js +++ b/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js @@ -16,7 +16,8 @@ export default class AttendanceReport extends Component { endDate: "", attendanceData: [], // Initialized as an empty array groupedData: [], // To store the grouped attendance data by employee_id - employeeIDS: [] // List of employee IDs to bind with select dropdown + employeeIDS: [], // List of employee IDs to bind with select dropdown + departmentIDS: [] }); onWillStart(async () => { @@ -34,6 +35,7 @@ export default class AttendanceReport extends Component { }); onMounted( () => { this.loademployeeIDS(); + this.loaddepartmentIDS(); }); } @@ -53,19 +55,63 @@ export default class AttendanceReport extends Component { } } - // Initialize Select2 with error handling and ensuring it's initialized only once - initializeSelect2() { - const employeeIDS = this.state.employeeIDS; + async loaddepartmentIDS() { + try { + const department = await this.orm.searchRead('hr.department', [], ['id', 'display_name']); + this.state.departmentIDS = department; - // Ensure the element is initialized only once + const $deptSelect = $('#dept'); + const $empSelect = $('#emp'); + const from_date = $("#from_date").datepicker({ + dateFormat: "dd/mm/yy", // Date format + showAnim: "slideDown", // Animation + changeMonth: true, // Allow month selection + changeYear: true, // Allow year selection + yearRange: "2010:2030", // Year range + // ... other options }); const to_date = $("#to_date").datepicker({ @@ -78,21 +124,45 @@ export default class AttendanceReport extends Component { }); // Debugging the employeeIDS array to verify its structure console.log("employeeIDS:", employeeIDS); + console.log("departmentIDS:", departmentIDS); + if (Array.isArray(departmentIDS) && departmentIDS.length > 0){ + $deptSelect.empty(); + + $deptSelect.append( + `` + ); + departmentIDS.forEach(dept => { + $deptSelect.append( + `` + ); + }); + + $deptSelect.on('change', (ev) => { + const selectedDepartmentIds = $(ev.target).val(); + console.log('Selected Department IDs: ', selectedDepartmentIds); + + const selectedDepartments = departmentIDS.filter(dept => selectedDepartmentIds.includes(dept.id.toString())); + console.log('Selected Department: ', selectedDepartments); + }) + } else { + console.error("Invalid department data format:", departmentIDS); + } // Check if employeeIDS is an array and has the necessary properties if (Array.isArray(employeeIDS) && employeeIDS.length > 0) { // Clear the current options (if any) $empSelect.empty(); + + $empSelect.append( + `` + ); // Add options for each employee employeeIDS.forEach(emp => { $empSelect.append( `` ); }); - $empSelect.append( - `` - ); // Initialize the select with the 'multiple' attribute for multi-select // $empSelect.attr('multiple', 'multiple'); @@ -126,9 +196,8 @@ export default class AttendanceReport extends Component { domain.push(['employee_id', '=', parseInt($('#emp').val())]); } try { - debugger; // Fetch the attendance data based on the date range and selected employees - const URL = await this.orm.call('attendance.report', 'export_to_excel', [$('#emp').val(), startdate, enddate]); + const URL = await this.orm.call('attendance.report', 'export_to_excel', [$('#dept').val(), $('#emp').val(), startdate, enddate]); window.open(getOrigin()+URL, '_blank'); } catch (error) { @@ -161,10 +230,13 @@ export default class AttendanceReport extends Component { try { // Fetch the attendance data based on the date range and selected employees // const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']); - const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#emp').val(),startDate,endDate]); + const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]); // Group data by employee_id - const groupedData = this.groupDataByEmployee(attendanceData); + const rawGroups = this.groupDataByEmployee(attendanceData); + + // Annotate week rows inside each group + const groupedData = rawGroups.map(group => this.annotateWeekRowspan(group)); // Update state with the fetched and grouped data this.state.attendanceData = attendanceData; diff --git a/addons_extensions/hr_attendance_extended/static/src/xml/attendance_report.xml b/addons_extensions/hr_attendance_extended/static/src/xml/attendance_report.xml index 18086f7dc..f19d0884a 100644 --- a/addons_extensions/hr_attendance_extended/static/src/xml/attendance_report.xml +++ b/addons_extensions/hr_attendance_extended/static/src/xml/attendance_report.xml @@ -15,6 +15,10 @@

Attendance Report