[IMP] hr_attendance: Display checked in hours today

Purpose
======
When an empoyee wants to check out, he does not know the number of hours he worked today.

Specification
==========
In the check out view, display how many hours the employee was checked in today.

closes odoo/odoo#33974

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Lucas Lefèvre
2019-06-26 06:52:31 +00:00
committed by Yannick Tivisse
parent 242be00429
commit ee1776dc5f
8 changed files with 63 additions and 1 deletions
@@ -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:
@@ -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 {
@@ -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 () {
@@ -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)]);
@@ -42,6 +42,7 @@
</div>
<h1 class="mb8"><t t-esc="widget.employee.name"/></h1>
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.hours_today"/></h4>
<a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-out btn-warning" t-if="checked_in" aria-label="Sign out" title="Sign out"/>
<a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-in btn-secondary" t-if="!checked_in" aria-label="Sign in" title="Sign in"/>
<h3 class="mt0 mb0 text-muted">Click to <b t-if="checked_in">check out</b><b t-if="!checked_in">check in</b></h3>
@@ -69,6 +70,7 @@
</div>
<h1 class="mb8"><t t-esc="widget.employee_name"/></h1>
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.employee_hours_today"/></h4>
<t t-if="!widget.use_pin">
<a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-out btn-warning" t-if="checked_in" aria-label="Sign out" title="Sign out"/>
<a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-in btn-secondary" t-if="!checked_in" aria-label="Sign in" title="Sign in"/>
@@ -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,
@@ -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")
@@ -101,6 +101,7 @@
<field name="arch" type="xml">
<kanban class="o_hr_employee_attendance_kanban" create="false">
<field name="attendance_state"/>
<field name="hours_today"/>
<field name="id"/>
<templates>
<t t-name="kanban-box">