diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index def4f72c2d4..c526230dc93 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import pytz from datetime import datetime from dateutil.relativedelta import relativedelta @@ -14,6 +15,7 @@ class HrEmployeeBase(models.AbstractModel): last_attendance_id = fields.Many2one('hr.attendance', compute='_compute_last_attendance_id', store=True) attendance_state = fields.Selection(string="Attendance Status", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")]) hours_last_month = fields.Float(compute='_compute_hours_last_month') + hours_today = fields.Float(compute='_compute_hours_today') def _compute_hours_last_month(self): for employee in self: @@ -27,6 +29,28 @@ class HrEmployeeBase(models.AbstractModel): ]) employee.hours_last_month = sum(attendances.mapped('worked_hours')) + def _compute_hours_today(self): + now = fields.Datetime.now() + now_utc = pytz.utc.localize(now) + for employee in self: + # start of day in the employee's timezone might be the previous day in utc + tz = pytz.timezone(employee.tz) + now_tz = now_utc.astimezone(tz) + start_tz = now_tz + relativedelta(hour=0, minute=0) # day start in the employee's timezone + start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None) + + attendances = self.env['hr.attendance'].search([ + ('employee_id', '=', employee.id), + ('check_in', '<=', now), + '|', ('check_out', '>=', start_naive), ('check_out', '=', False), + ]) + + worked_hours = 0 + for attendance in attendances: + delta = (attendance.check_out or now) - max(attendance.check_in, start_naive) + worked_hours += delta.total_seconds() / 3600.0 + employee.hours_today = worked_hours + @api.depends('attendance_ids') def _compute_last_attendance_id(self): for employee in self: diff --git a/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js b/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js index 9949b9d8db2..eb9133e5d21 100644 --- a/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js +++ b/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js @@ -23,6 +23,7 @@ KanbanRecord.include({ employee_id: this.record.id.raw_value, employee_name: this.record.name.raw_value, employee_state: this.record.attendance_state.raw_value, + employee_hours_today: this.record.hours_today.raw_value, }; this.do_action(action); } else { diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js index c7640527297..8262b24c8f8 100644 --- a/addons/hr_attendance/static/src/js/kiosk_confirm.js +++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js @@ -3,6 +3,7 @@ odoo.define('hr_attendance.kiosk_confirm', function (require) { var AbstractAction = require('web.AbstractAction'); var core = require('web.core'); +var field_utils = require('web.field_utils'); var QWeb = core.qweb; @@ -61,6 +62,7 @@ var KioskConfirm = AbstractAction.extend({ this.employee_id = action.employee_id; this.employee_name = action.employee_name; this.employee_state = action.employee_state; + this.employee_hours_today = field_utils.format.float_time(action.employee_hours_today); }, start: function () { diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index 5bf9884875d..91acb7f1599 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -3,6 +3,7 @@ odoo.define('hr_attendance.my_attendances', function (require) { var AbstractAction = require('web.AbstractAction'); var core = require('web.core'); +var field_utils = require('web.field_utils'); var MyAttendances = AbstractAction.extend({ @@ -19,10 +20,13 @@ var MyAttendances = AbstractAction.extend({ var def = this._rpc({ model: 'hr.employee', method: 'search_read', - args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name']], + args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name', 'hours_today']], }) .then(function (res) { self.employee = res.length && res[0]; + if (res.length) { + self.hours_today = field_utils.format.float_time(self.employee.hours_today); + } }); return Promise.all([def, this._super.apply(this, arguments)]); diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml index 3becfa5cccd..f38dd7cfa7c 100644 --- a/addons/hr_attendance/static/src/xml/attendance.xml +++ b/addons/hr_attendance/static/src/xml/attendance.xml @@ -42,6 +42,7 @@

Welcome!Want to check out?

+

Today's work hours:

Click to check outcheck in

@@ -69,6 +70,7 @@

Welcome!Want to check out?

+

Today's work hours:

diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js index a1cf6955791..22de18817ad 100644 --- a/addons/hr_attendance/static/tests/hr_attendance_tests.js +++ b/addons/hr_attendance/static/tests/hr_attendance_tests.js @@ -23,6 +23,7 @@ QUnit.module('HR Attendance', { }, user_id: {string: 'user ID', type: 'integer'}, barcode: {string:'barcode', type: 'integer'}, + hours_today: {string:'Hours today', type: 'float'}, }, records: [{ id: 1, diff --git a/addons/hr_attendance/tests/test_hr_attendance_process.py b/addons/hr_attendance/tests/test_hr_attendance_process.py index dd9d5b90dbd..91b3186b895 100644 --- a/addons/hr_attendance/tests/test_hr_attendance_process.py +++ b/addons/hr_attendance/tests/test_hr_attendance_process.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- +import pytz +from datetime import datetime +from unittest.mock import patch + +from odoo import fields from odoo.tests import new_test_user from odoo.tests.common import TransactionCase @@ -81,3 +86,25 @@ class TestHrAttendance(TransactionCase): self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin") employee.attendance_manual({}, entered_pin=None) self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin") + + def test_hours_today(self): + """ Test day start is correctly computed according to the employee's timezone """ + + def tz_datetime(year, month, day, hour, minute): + tz = pytz.timezone('Europe/Brussels') + return tz.localize(datetime(year, month, day, hour, minute)).astimezone(pytz.utc) + + employee = self.env['hr.employee'].create({'name': 'Cunégonde', 'tz': 'Europe/Brussels'}) + self.env['hr.attendance'].create({ + 'employee_id': employee.id, + 'check_in': tz_datetime(2019, 3, 1, 22, 0), # should count from midnight in the employee's timezone (=the previous day in utc!) + 'check_out': tz_datetime(2019, 3, 2, 2, 0), + }) + self.env['hr.attendance'].create({ + 'employee_id': employee.id, + 'check_in': tz_datetime(2019, 3, 2, 11, 0), + }) + + # now = 2019/3/2 14:00 in the employee's timezone + with patch.object(fields.Datetime, 'now', lambda: tz_datetime(2019, 3, 2, 14, 0).astimezone(pytz.utc).replace(tzinfo=None)): + self.assertEqual(employee.hours_today, 5, "It should have counted 5 hours") diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml index 6e6b6d7df56..7603422ef4b 100644 --- a/addons/hr_attendance/views/hr_employee_view.xml +++ b/addons/hr_attendance/views/hr_employee_view.xml @@ -101,6 +101,7 @@ +