Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d9d985d

Browse files
committedMay 22, 2025
[FIX] hr_attendance: count lunch intervals in attendance auto check out
**Issue** For example, in the case of a working schedule from 8:00 to 17:00 with a 1 hour lunch period, 8 hours of work are expected. By taking the lunch interval into account, the auto check out will happen at 17:00 if the company's tolerance in the attendance's setting is set at 0. Previously, it would happen at 16:00, resulting in only 7 worked hours for the attendance while the user may expect the attendance's worked hours to match the expected working hours of the schedule. **Solution** - include lunch attendances (not taken into account by `duration_hours`) in the total time that needs to be exceeded before an `hr.attendance` is automatically checked out. Note that this solution manages the case where `hr.attendance` are outside lunch periods (e.g. 8-12 and 13-17, but no attendance between 12-13). Though in that case, the automatic check out will be delayed by at least 1h. Note: a change was also made to account for 2 weeks calendars. opw-4402321 closes odoo#210921 X-original-commit: 179c043 Signed-off-by: Tanguy Quéguineur (taqu) <[email protected]> Signed-off-by: Bertrand Dossogne (bedo) <[email protected]>
1 parent f58d565 commit d9d985d

File tree

2 files changed

+64
-6
lines changed

2 files changed

+64
-6
lines changed
 

‎addons/hr_attendance/models/hr_attendance.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -709,14 +709,16 @@ def _cron_auto_check_out(self):
709709
max_tol = company.auto_check_out_tolerance
710710
to_verify_company = to_verify.filtered(lambda a: a.employee_id.company_id.id == company.id)
711711

712-
# Attendances where Last open attendance worked time + previously worked time on that day + tolerance greater than the planned worked hours in his calendar
713-
to_check_out = to_verify_company.filtered(lambda a: (fields.Datetime.now() - a.check_in).seconds / 3600 + mapped_previous_duration[a.employee_id][a.check_in.date()] - max_tol > (sum(a.employee_id.resource_calendar_id.attendance_ids.filtered(lambda att: att.dayofweek == str(a.check_in.weekday())).mapped('duration_hours'))))
712+
# Attendances where Last open attendance time + previously worked time on that day + tolerance greater than the attendances hours (including lunch) in his calendar
713+
to_check_out = to_verify_company.filtered(lambda a: (fields.Datetime.now() - a.check_in).seconds / 3600 + mapped_previous_duration[a.employee_id][a.check_in.date()] - max_tol > (sum(a.employee_id.resource_calendar_id.attendance_ids.filtered(lambda att: att.dayofweek == str(a.check_in.weekday()) and (not att.two_weeks_calendar or att.week_type == str(att.get_week_type(a.check_in.date())))).mapped(lambda at: at.hour_to - at.hour_from))))
714714
body = _('This attendance was automatically checked out because the employee exceeded the allowed time for their scheduled work hours.')
715715

716716
for att in to_check_out:
717-
delta_duration = max(1, (sum(att.employee_id.resource_calendar_id.attendance_ids.filtered(lambda a: a.dayofweek == str(att.check_in.weekday())).mapped('duration_hours')) + max_tol - mapped_previous_duration[att.employee_id][att.check_in.date()]) * 3600)
717+
expected_worked_hours = sum(att.employee_id.resource_calendar_id.attendance_ids.filtered(lambda a: a.dayofweek == str(att.check_in.weekday()) and (not a.two_weeks_calendar or a.week_type == str(a.get_week_type(att.check_in.date())))).mapped("duration_hours"))
718+
att.check_out = fields.Datetime.now()
719+
excess_hours = att.worked_hours - (expected_worked_hours + max_tol - mapped_previous_duration[att.employee_id][att.check_in.date()])
718720
att.write({
719-
"check_out": att.check_in + relativedelta(seconds=delta_duration),
721+
"check_out": max(att.check_out - relativedelta(hours=excess_hours), att.check_in + relativedelta(seconds=1)),
720722
"out_mode": "auto_check_out"
721723
})
722724
att.message_post(body=body)

‎addons/hr_attendance/tests/test_hr_attendance_overtime.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,17 @@ def test_auto_check_out(self):
418418
'auto_check_out': True,
419419
'auto_check_out_tolerance': 1
420420
})
421-
self.env['hr.attendance'].create({
421+
self.env['hr.attendance'].create([{
422422
'employee_id': self.employee.id,
423423
'check_in': datetime(2024, 2, 1, 8, 0),
424+
'check_out': datetime(2024, 2, 1, 11, 0)
425+
},
426+
{
427+
'employee_id': self.employee.id,
428+
'check_in': datetime(2024, 2, 1, 11, 0),
424429
'check_out': datetime(2024, 2, 1, 13, 0)
425-
})
430+
}
431+
])
426432

427433
attendance_utc_pending = self.env['hr.attendance'].create({
428434
'employee_id': self.employee.id,
@@ -467,6 +473,56 @@ def test_auto_check_out(self):
467473
# Employee with flexible working schedule should not be checked out
468474
self.assertEqual(attendance_flexible_pending.check_out, False)
469475

476+
def test_auto_check_out_lunch_period(self):
477+
Attendance = self.env['hr.attendance']
478+
self.company.write({
479+
'auto_check_out': True,
480+
'auto_check_out_tolerance': 1
481+
})
482+
morning, afternoon = Attendance.create([{
483+
'employee_id': self.employee.id,
484+
'check_in': datetime(2024, 1, 1, 8, 0),
485+
'check_out': datetime(2024, 1, 1, 12, 0)
486+
},
487+
{
488+
'employee_id': self.employee.id,
489+
'check_in': datetime(2024, 1, 1, 13, 0)
490+
}])
491+
492+
with freeze_time("2024-01-01 22:00:00"):
493+
Attendance._cron_auto_check_out()
494+
self.assertEqual(morning.worked_hours + afternoon.worked_hours, 9) # 8 hours from calendar's attendances + 1 hour of tolerance
495+
self.assertEqual(afternoon.check_out, datetime(2024, 1, 1, 18, 0))
496+
497+
def test_auto_check_out_two_weeks_calendar(self):
498+
"""Test case: two weeks calendar with different attendances depending on the week. No morning attendance on
499+
wednesday of the first week."""
500+
Attendance = self.env['hr.attendance']
501+
self.company.write({
502+
'auto_check_out': True,
503+
'auto_check_out_tolerance': 0
504+
})
505+
self.employee.resource_calendar_id.switch_calendar_type()
506+
self.employee.resource_calendar_id.attendance_ids.search([("dayofweek", "=", "2"), ("week_type", '=', '0'), ("day_period", "in", ["morning", "lunch"])]).unlink()
507+
508+
with freeze_time("2025-03-05 22:00:00"):
509+
att = Attendance.create({
510+
'employee_id': self.employee.id,
511+
'check_in': datetime(2025, 3, 5, 8, 0)
512+
})
513+
Attendance._cron_auto_check_out()
514+
self.assertEqual(att.worked_hours, 4)
515+
self.assertEqual(att.check_out, datetime(2025, 3, 5, 12, 0))
516+
517+
with freeze_time("2025-03-12 22:00:00"):
518+
att = Attendance.create({
519+
'employee_id': self.employee.id,
520+
'check_in': datetime(2025, 3, 12, 8, 0),
521+
})
522+
Attendance._cron_auto_check_out()
523+
self.assertEqual(att.worked_hours, 8)
524+
self.assertEqual(att.check_out, datetime(2025, 3, 12, 17, 0))
525+
470526
@freeze_time("2024-02-1 14:00:00")
471527
def test_absence_management(self):
472528
self.company.write({

0 commit comments

Comments
 (0)
Please sign in to comment.