Added Break Hours data in attendance report

This commit is contained in:
pranay 2025-08-29 10:39:59 +05:30
parent 457867f3b0
commit ebbfb0c445
3 changed files with 134 additions and 116 deletions

View File

@ -61,111 +61,116 @@ class AttendanceReport(models.Model):
# Define the query # Define the query
query = """ query = """
WITH date_range AS ( WITH date_range AS (
SELECT generate_series( SELECT generate_series(
%s::date, %s::date,
%s::date, %s::date,
interval '1 day' interval '1 day'
)::date AS date )::date AS date
), ),
employee_dates AS ( employee_dates AS (
SELECT SELECT
emp.id AS employee_id, emp.id AS employee_id,
emp.name AS employee_name, emp.name AS employee_name,
dr.date, dr.date,
TO_CHAR(dr.date, 'Day') AS day_name, TO_CHAR(dr.date, 'Day') AS day_name,
EXTRACT(WEEK FROM dr.date) AS week_number, EXTRACT(WEEK FROM dr.date) AS week_number,
TO_CHAR(date_trunc('week', dr.date), 'MON DD') || ' - ' || TO_CHAR(date_trunc('week', dr.date), 'MON DD') || ' - ' ||
TO_CHAR(date_trunc('week', dr.date) + interval '6 days', 'MON DD') AS week_range, TO_CHAR(date_trunc('week', dr.date) + interval '6 days', 'MON DD') AS week_range,
dep.name->>'en_US' AS department dep.name->>'en_US' AS department
FROM FROM
hr_employee emp hr_employee emp
CROSS JOIN CROSS JOIN
date_range dr date_range dr
LEFT JOIN LEFT JOIN
hr_department dep ON emp.department_id = dep.id hr_department dep ON emp.department_id = dep.id
WHERE WHERE
emp.active = true emp.active = true
""" + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """ """ + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """
), ),
daily_checkins AS ( daily_checkins AS (
SELECT SELECT
emp.id, emp.id,
emp.name, emp.name,
DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date,
at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in, at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in,
at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out, at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out,
at.worked_hours, 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) 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 dep.name->>'en_US' AS department,
FROM LEAD(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS next_check_in
hr_attendance at FROM
LEFT JOIN hr_attendance at
hr_employee emp ON at.employee_id = emp.id LEFT JOIN
LEFT JOIN hr_employee emp ON at.employee_id = emp.id
hr_department dep ON emp.department_id = dep.id LEFT JOIN
WHERE hr_department dep ON emp.department_id = dep.id
""" + " AND ".join(checkin_conditions) + """ WHERE
), """ + " AND ".join(checkin_conditions) + """
attendance_summary AS ( ),
SELECT attendance_summary AS (
id, SELECT
name, id,
date, name,
MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in, date,
MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out, MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in,
SUM(worked_hours) AS total_worked_hours, MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out,
department SUM(worked_hours) AS total_worked_hours,
FROM -- 👇 Calculate total break time (sum of gaps between check_out and next check_in)
daily_checkins SUM(
GROUP BY EXTRACT(EPOCH FROM (next_check_in - check_out)) / 3600
id, name, date, department ) FILTER (WHERE next_check_in IS NOT NULL AND check_out IS NOT NULL) AS break_hours,
), department
leave_data AS ( FROM
SELECT daily_checkins
hl.employee_id, GROUP BY
hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start, id, name, date, department
hl.date_to AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_end, ),
hlt.name->>'en_US' AS leave_type, leave_data AS (
hl.request_unit_half AS is_half_day, SELECT
hl.request_date_from, hl.employee_id,
hl.request_date_to hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start,
FROM hl.date_to AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_end,
hr_leave hl hlt.name->>'en_US' AS leave_type,
JOIN hl.request_unit_half AS is_half_day,
hr_leave_type hlt ON hl.holiday_status_id = hlt.id hl.request_date_from,
WHERE hl.request_date_to
hl.state IN ('validate', 'confirm', 'validate1') FROM
AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp) hr_leave hl
) JOIN
SELECT hr_leave_type hlt ON hl.holiday_status_id = hlt.id
ed.employee_id AS id, WHERE
ed.employee_name AS name, hl.state IN ('validate', 'confirm', 'validate1')
ed.date, AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp)
'Week ' || ed.week_number || ' (' || ed.week_range || ')' AS week_info, )
TRIM(ed.day_name) AS day_name, SELECT
COALESCE(ats.first_check_in, NULL) AS first_check_in, ed.employee_id AS id,
COALESCE(ats.last_check_out, NULL) AS last_check_out, ed.employee_name AS name,
COALESCE(ats.total_worked_hours, 0) AS total_worked_hours, ed.date,
ed.department, 'Week ' || ed.week_number || ' (' || ed.week_range || ')' AS week_info,
CASE TRIM(ed.day_name) AS day_name,
WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type COALESCE(ats.first_check_in, NULL) AS first_check_in,
WHEN ld.leave_type IS NOT NULL THEN 'on ' || ld.leave_type COALESCE(ats.last_check_out, NULL) AS last_check_out,
WHEN ats.first_check_in IS NOT NULL THEN 'Present' COALESCE(ats.total_worked_hours, 0) AS total_worked_hours,
ELSE 'NA' COALESCE(ats.break_hours, 0) AS total_break_hours,
END AS status ed.department,
FROM CASE
employee_dates ed WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type
LEFT JOIN WHEN ld.leave_type IS NOT NULL THEN 'on ' || ld.leave_type
attendance_summary ats ON ed.employee_id = ats.id AND ed.date = ats.date WHEN ats.first_check_in IS NOT NULL THEN 'Present'
LEFT JOIN ELSE 'NA'
leave_data ld ON ed.employee_id = ld.employee_id END AS status
AND ed.date >= DATE(ld.leave_start) FROM
AND ed.date <= DATE(ld.leave_end) employee_dates ed
ORDER BY LEFT JOIN
ed.employee_id, ed.date; 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
ed.employee_id, ed.date;
""" """
# Combine all parameters in the correct order: # Combine all parameters in the correct order:
# 1. date_range params (start_date_str, end_date_str) # 1. date_range params (start_date_str, end_date_str)
# 2. employee_dates params (emp_date_params) # 2. employee_dates params (emp_date_params)
@ -196,6 +201,7 @@ class AttendanceReport(models.Model):
'check_in': r['first_check_in'], 'check_in': r['first_check_in'],
'check_out': r['last_check_out'], 'check_out': r['last_check_out'],
'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0, 'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0,
'break_hours': float(r['total_break_hours']) if r['total_break_hours'] is not None else 0.0,
'status': r['status'] 'status': r['status']
}) })
@ -213,7 +219,6 @@ class AttendanceReport(models.Model):
attendance_data = self.get_attendance_report(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: if not attendance_data:
raise UserError("No data to export!") raise UserError("No data to export!")
# Create workbook and sheet # Create workbook and sheet
workbook = xlwt.Workbook(encoding='utf-8') workbook = xlwt.Workbook(encoding='utf-8')
sheet = workbook.add_sheet('Attendance Report') sheet = workbook.add_sheet('Attendance Report')
@ -281,28 +286,26 @@ class AttendanceReport(models.Model):
) )
# Set column widths (in units of 1/256 of a character width) # Set column widths (in units of 1/256 of a character width)
col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 5000] col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 4000, 5000]
for i, width in enumerate(col_widths): for i, width in enumerate(col_widths):
sheet.col(i).width = width sheet.col(i).width = width
# Write title # Write title
sheet.write_merge(0, 0, 0, 8, 'ATTENDANCE REPORT', title_style) sheet.write_merge(0, 0, 0, 9, 'ATTENDANCE REPORT', title_style)
# Write date range # Write date range
date_range = f"From: {start_date} To: {end_date}" date_range = f"From: {start_date} To: {end_date}"
sheet.write_merge(1, 1, 0, 8, date_range, xlwt.easyxf( sheet.write_merge(1, 1, 0, 9, date_range, xlwt.easyxf(
'font: italic on; align: horiz center' 'font: italic on; align: horiz center'
)) ))
# Write headers # Write headers
headers = [ headers = [
'Department', 'Employee Name','Week', 'Date', 'Day', 'Department', 'Employee Name','Week', 'Date', 'Day',
'Check-in', 'Check-out', 'Worked Hours', 'Status' 'Check-in', 'Check-out', 'Worked Hours', 'Break Hours', 'Status'
] ]
for col_num, header in enumerate(headers): for col_num, header in enumerate(headers):
sheet.write(2, col_num, header, header_style) sheet.write(2, col_num, header, header_style)
# Write data rows # Write data rows
current_employee = None current_employee = None
for row_num, record in enumerate(attendance_data, start=3): for row_num, record in enumerate(attendance_data, start=3):
@ -345,11 +348,16 @@ class AttendanceReport(models.Model):
else: else:
sheet.write(row_num, 7, str(record['worked_hours']), data_style) sheet.write(row_num, 7, str(record['worked_hours']), data_style)
sheet.write(row_num, 8, record['status'], status_style) # Break hours formatting
if isinstance(record['break_hours'], (float, int)):
sheet.write(row_num, 8, float(record['break_hours']), hours_style)
else:
sheet.write(row_num, 8, str(record['break_hours']), data_style)
sheet.write(row_num, 9, record['status'], status_style)
# Add freeze panes (headers will stay visible when scrolling) # Add freeze panes (headers will stay visible when scrolling)
sheet.set_panes_frozen(True) sheet.set_panes_frozen(True)
sheet.set_horz_split_pos(4) # After row 3 (headers) sheet.set_horz_split_pos(3) # After row 3 (headers)
sheet.set_vert_split_pos(0) # No vertical split sheet.set_vert_split_pos(0) # No vertical split
# Save to buffer # Save to buffer

View File

@ -206,7 +206,7 @@ export default class AttendanceReport extends Component {
} }
async generateReport() { async generateReport() {
debugger;
let { startDate, endDate, selectedEmployeeIds } = this.state; let { startDate, endDate, selectedEmployeeIds } = this.state;
startDate = $('#from_date').val() startDate = $('#from_date').val()
endDate = $('#to_date').val() endDate = $('#to_date').val()
@ -231,7 +231,7 @@ export default class AttendanceReport extends Component {
// Fetch the attendance data based on the date range and selected employees // 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.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']);
const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]); const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]);
debugger;
// Group data by employee_id // Group data by employee_id
const rawGroups = this.groupDataByEmployee(attendanceData); const rawGroups = this.groupDataByEmployee(attendanceData);

View File

@ -65,6 +65,7 @@
<th>Check In</th> <th>Check In</th>
<th>Check Out</th> <th>Check Out</th>
<th>Worked Hours</th> <th>Worked Hours</th>
<th>Break Hours</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
@ -78,7 +79,16 @@
<td><t t-esc="data.day_name"/></td> <td><t t-esc="data.day_name"/></td>
<td><t t-esc="data.check_in"/></td> <td><t t-esc="data.check_in"/></td>
<td><t t-esc="data.check_out"/></td> <td><t t-esc="data.check_out"/></td>
<td><t t-esc="data.worked_hours"/></td> <td>
<t t-set="hours" t-value="Math.floor(data.worked_hours)"/>
<t t-set="minutes" t-value="Math.round((data.worked_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td>
<t t-set="hours" t-value="Math.floor(data.break_hours)"/>
<t t-set="minutes" t-value="Math.round((data.break_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td><t t-esc="data.status"/></td> <td><t t-esc="data.status"/></td>
</tr> </tr>
</tbody> </tbody>