Skip to content

Commit d5be57c

Browse files
[FIX] hr_attendance: auto-checkout on different date
Problem: the auto-checkout feature was basing the computation on the fact that the unclosed attendance was starting today. However, it might not always be the case, for example if the server is shutdown for more than 24 hours after checking in. Steps to reproduce: - Activate the auto-checkout feature - Create an open-ended attendance for two days ago - Run the cron - Result: the attendance is not closed. This commit solves the issue by taking into account the days delta between today and the check-in date. task-5082359 closes odoo#227981 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
1 parent 412e492 commit d5be57c

File tree

2 files changed

+60
-16
lines changed

2 files changed

+60
-16
lines changed

addons/hr_attendance/models/hr_attendance.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ def action_refuse_overtime(self):
707707
def _cron_auto_check_out(self):
708708
def check_in_tz(attendance):
709709
"""Returns check-in time in calendar's timezone."""
710-
return attendance.check_in.astimezone(pytz.timezone(attendance.employee_id.resource_calendar_id.tz or 'UTC'))
710+
return attendance.check_in.astimezone(pytz.timezone(attendance.employee_id._get_tz()))
711711

712712
to_verify = self.env['hr.attendance'].search(
713713
[('check_out', '=', False),
@@ -718,9 +718,10 @@ def check_in_tz(attendance):
718718
if not to_verify:
719719
return
720720

721+
to_verify_min_date = min(to_verify.mapped('check_in')).replace(hour=0, minute=0, second=0)
721722
previous_attendances = self.env['hr.attendance'].search([
722723
('employee_id', 'in', to_verify.mapped('employee_id').ids),
723-
('check_in', '>', (fields.Datetime.now() - relativedelta(days=1)).replace(hour=0, minute=0, second=0)),
724+
('check_in', '>', to_verify_min_date),
724725
('check_out', '!=', False)
725726
])
726727

@@ -734,20 +735,32 @@ def check_in_tz(attendance):
734735
max_tol = company.auto_check_out_tolerance
735736
to_verify_company = to_verify.filtered(lambda a: a.employee_id.company_id.id == company.id)
736737

737-
# Attendances where Last open attendance time + previously worked time on that day + tolerance greater than the attendances hours (including lunch) in his calendar
738-
to_check_out = to_verify_company.filtered(lambda a: (fields.Datetime.now() - a.check_in).seconds / 3600 + mapped_previous_duration[a.employee_id][check_in_tz(a).date()] - max_tol >
739-
(sum(a.employee_id.resource_calendar_id.attendance_ids.filtered(lambda att: att.dayofweek == str(check_in_tz(a).weekday()) and (not att.two_weeks_calendar or att.week_type == str(att.get_week_type(check_in_tz(a).date())))).mapped(lambda at: at.hour_to - at.hour_from))))
740-
body = _('This attendance was automatically checked out because the employee exceeded the allowed time for their scheduled work hours.')
741-
742-
for att in to_check_out:
743-
expected_worked_hours = sum(att.employee_id.resource_calendar_id.attendance_ids.filtered(lambda a: a.dayofweek == str(check_in_tz(att).weekday()) and (not a.two_weeks_calendar or a.week_type == str(a.get_week_type(check_in_tz(att).date())))).mapped("duration_hours"))
744-
att.check_out = fields.Datetime.now()
745-
excess_hours = att.worked_hours - (expected_worked_hours + max_tol - mapped_previous_duration[att.employee_id][check_in_tz(att).date()])
746-
att.write({
747-
"check_out": max(att.check_out - relativedelta(hours=excess_hours), att.check_in + relativedelta(seconds=1)),
748-
"out_mode": "auto_check_out"
749-
})
750-
att.message_post(body=body)
738+
for att in to_verify_company:
739+
740+
employee_timezone = pytz.timezone(att.employee_id._get_tz())
741+
check_in_datetime = check_in_tz(att)
742+
now_datetime = fields.Datetime.now().astimezone(employee_timezone)
743+
current_attendance_duration = (now_datetime - check_in_datetime).total_seconds() / 3600
744+
previous_attendances_duration = mapped_previous_duration[att.employee_id][check_in_datetime.date()]
745+
746+
expected_worked_hours = sum(
747+
att.employee_id.resource_calendar_id.attendance_ids.filtered(
748+
lambda a: a.dayofweek == str(check_in_datetime.weekday())
749+
and (not a.two_weeks_calendar or a.week_type == str(a.get_week_type(check_in_datetime.date())))
750+
).mapped("duration_hours")
751+
)
752+
753+
# Attendances where Last open attendance time + previously worked time on that day + tolerance greater than the attendances hours (including lunch) in his calendar
754+
if (current_attendance_duration + previous_attendances_duration - max_tol) > expected_worked_hours:
755+
att.check_out = att.check_in.replace(hour=23, minute=59, second=59)
756+
excess_hours = att.worked_hours - (expected_worked_hours + max_tol - previous_attendances_duration)
757+
att.write({
758+
"check_out": max(att.check_out - relativedelta(hours=excess_hours), att.check_in + relativedelta(seconds=1)),
759+
"out_mode": "auto_check_out"
760+
})
761+
att.message_post(
762+
body=_('This attendance was automatically checked out because the employee exceeded the allowed time for their scheduled work hours.')
763+
)
751764

752765
def _cron_absence_detection(self):
753766
"""

addons/hr_attendance/tests/test_hr_attendance_overtime.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,37 @@ def test_auto_check_out(self):
444444
# Employee with flexible working schedule should not be checked out
445445
self.assertEqual(attendance_flexible_pending.check_out, False)
446446

447+
@freeze_time("2024-02-1 23:00:00")
448+
def test_auto_check_out_more_one_day_delta(self):
449+
""" Test that the checkout is correct if the delta between the check in and now is > 24 hours"""
450+
self.company.write({
451+
'auto_check_out': True,
452+
'auto_check_out_tolerance': 1
453+
})
454+
455+
attendance_utc_pending = self.env['hr.attendance'].create({
456+
'employee_id': self.employee.id,
457+
'check_in': datetime(2024, 1, 30, 8, 0)
458+
})
459+
460+
self.assertEqual(attendance_utc_pending.check_out, False)
461+
self.env['hr.attendance']._cron_auto_check_out()
462+
self.assertEqual(attendance_utc_pending.check_out, datetime(2024, 1, 30, 18, 0))
463+
464+
@freeze_time("2024-02-05 23:00:00")
465+
def test_auto_checkout_past_day(self):
466+
self.company.write({
467+
'auto_check_out': True,
468+
'auto_check_out_tolerance': 1,
469+
})
470+
attendance_utc_pending_7th_day = self.env['hr.attendance'].create({
471+
'employee_id': self.employee.id,
472+
'check_in': datetime(2024, 2, 1, 14, 0),
473+
})
474+
self.assertEqual(attendance_utc_pending_7th_day.check_out, False)
475+
self.env['hr.attendance']._cron_auto_check_out()
476+
self.assertEqual(attendance_utc_pending_7th_day.check_out, datetime(2024, 2, 1, 23, 0))
477+
447478
@freeze_time("2024-02-2 20:00:00")
448479
def test_auto_check_out_calendar_tz(self):
449480
"""Check expected working hours and previously worked hours are from the correct day when

0 commit comments

Comments
 (0)