[IMP] mail, hr_holidays: use leaves to compute im_status

When on leave, the im_status can be either leave_online or leave_offline.
This will allow to display a specific icon and a default message when the user
is not at the office.
The leave can have an out of office message. This message will be displayed
in the chat window instead of the user ooo message, and the end date of the leave will be displayed too.

Technical details.
Right now the im_status access leaves directly to know if a user is on leave or not.
An index has been added to optimise this request (thanks to rco).
A solution to optimise this would be to compute part of the information in a cron.

Task: 1856205
This commit is contained in:
XavierDo
2019-02-07 12:53:03 +00:00
parent c21d200285
commit 086405344e
14 changed files with 269 additions and 18 deletions
+4 -1
View File
@@ -6,4 +6,7 @@ from . import hr
from . import hr_leave
from . import hr_leave_allocation
from . import hr_leave_type
from . import mail_message_subtype
from . import mail_channel
from . import mail_message_subtype
from . import res_partner
from . import res_users
+9 -1
View File
@@ -9,7 +9,7 @@ import math
from datetime import datetime
from pytz import timezone, UTC
from odoo import api, fields, models
from odoo import api, fields, models, tools
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare
@@ -128,6 +128,7 @@ class HolidaysRequest(models.Model):
'hr.department', string='Department', readonly=True,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
out_of_office_message = fields.Char(string='Out of Office Message')
# duration
date_from = fields.Datetime(
'Start Date', readonly=True, index=True, copy=False, required=True,
@@ -250,6 +251,13 @@ class HolidaysRequest(models.Model):
('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"),
]
@api.model_cr_context
def _auto_init(self):
res = super(HolidaysRequest, self)._auto_init()
tools.create_index(self._cr, 'hr_leave_date_to_date_from_index',
self._table, ['date_to', 'date_from'])
return res
@api.onchange('holiday_status_id')
def _onchange_holiday_status_id(self):
self.request_unit_half = False
+36
View File
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class Channel(models.Model):
_inherit = 'mail.channel'
@api.multi
def channel_info(self, extra_info=False):
channel_infos = super(Channel, self).channel_info(extra_info)
partners_on_leave = []
for channel_info in channel_infos:
if 'direct_partner' in channel_info:
for direct_partner in channel_info['direct_partner']:
if 'leave' in direct_partner['im_status']:
partners_on_leave.append(direct_partner['id'])
if partners_on_leave:
now = fields.Datetime.now()
self.env.cr.execute('''SELECT res_users.partner_id as partner_id, hr_leave.out_of_office_message as out_of_office_message, hr_leave.date_to as date_to
FROM res_users
JOIN hr_leave ON hr_leave.user_id = res_users.id
AND hr_leave.state not in ('cancel', 'refuse')
AND res_users.active = 't'
AND hr_leave.date_from <= %s
AND hr_leave.date_to >= %s
AND res_users.partner_id in %s''', (now, now, tuple(partners_on_leave)))
out_of_office_info = dict(((res['partner_id'], res) for res in self.env.cr.dictfetchall()))
for channel_info in channel_infos:
if 'direct_partner' in channel_info:
for direct_partner in channel_info['direct_partner']:
if 'leave' in direct_partner['im_status']:
direct_partner['out_of_office_date_end'] = out_of_office_info.get(direct_partner['id'], {}).get('date_to')
direct_partner['out_of_office_message'] = out_of_office_info.get(direct_partner['id'], {}).get('out_of_office_message')
return channel_infos
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
def _compute_im_status(self):
super(ResPartner, self)._compute_im_status()
absent_now = self._get_on_leave_ids()
for partner in self:
if partner.id in absent_now:
if partner.im_status == 'online':
partner.im_status = 'leave_online'
else:
partner.im_status = 'leave_offline'
@api.model
def _get_on_leave_ids(self):
return self.env['res.users']._get_on_leave_ids(partner=True)
+29
View File
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResUsers(models.Model):
_inherit = "res.users"
def _compute_im_status(self):
super(ResUsers, self)._compute_im_status()
on_leave_user_ids = self._get_on_leave_ids()
for user in self:
if user.id in on_leave_user_ids:
if user.im_status == 'online':
user.im_status = 'leave_online'
else:
user.im_status = 'leave_offline'
@api.model
def _get_on_leave_ids(self, partner=False):
now = fields.Datetime.now()
field = 'partner_id' if partner else 'id'
self.env.cr.execute('''SELECT res_users.%s FROM res_users
JOIN hr_leave ON hr_leave.user_id = res_users.id
AND state not in ('cancel', 'refuse')
AND res_users.active = 't'
AND date_from <= %%s AND date_to >= %%s''' % field, (now, now))
return [r[0] for r in self.env.cr.fetchall()]
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-extend="mail.UserStatus">
<t t-jquery="i:last" t-operation="after">
<i t-if="status == 'leave_online'" class="o_mail_user_status o_user_online fa fa-plane" title="Online" role="img" aria-label="User is online"/>
<i t-if="status == 'leave_offline'" class="o_mail_user_status o_user_idle fa fa-plane" title="Out of office" role="img" aria-label="User is out of office"/>
</t>
</t>
</templates>
+1
View File
@@ -7,3 +7,4 @@ from . import test_hr_leave_type
from . import test_accrual_allocations
from . import test_change_department
from . import test_leave_requests
from . import test_out_of_office
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo.tests.common import tagged, users, warmup
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
@tagged('out_of_office')
class TestOutOfOffice(TestHrHolidaysBase):
def test_leave_ooo(self):
leave_type = self.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'allocation_type': 'no',
})
self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
leave_date_end = (datetime.today() + relativedelta(days=3))
leave = self.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': self.employee_hruser.id,
'holiday_status_id': leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': leave_date_end,
'out_of_office_message': 'contact tde in case of problems',
'number_of_days': 4,
})
leave.action_approve()
self.assertEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
self.assertEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
partner = self.employee_hruser.user_id.partner_id
partner2 = self.user_employee.partner_id
channel = self.env['mail.channel'].with_context({
'mail_create_nolog': True,
'mail_create_nosubscribe': True,
'mail_channel_noautofollow': True,
}).create({
'channel_partner_ids': [(4, partner.id), (4, partner2.id)],
'public': 'private',
'channel_type': 'chat',
'email_send': False,
'name': 'test'
})
infos = channel.sudo(self.user_employee).channel_info()
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_date_end'], leave_date_end)
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'contact tde in case of problems')
@tagged('out_of_office')
class TestOutOfOfficePerformance(TestHrHolidaysBase):
def setUp(self):
super(TestOutOfOfficePerformance, self).setUp()
self.leave_type = self.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'allocation_type': 'no',
})
self.leave_date_end = (datetime.today() + relativedelta(days=3))
self.leave = self.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': (datetime.today() + relativedelta(days=3)),
'out_of_office_message': 'contact tde in case of problems',
'number_of_days': 4,
})
self.hr_user = self.employee_hruser.user_id
self.hr_partner = self.employee_hruser.user_id.partner_id
self.employer_partner = self.user_employee.partner_id
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_partner_offline(self):
with self.assertQueryCount(__system__=2, demo=2):
self.assertEqual(self.employer_partner.im_status, 'offline')
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_user_leave_offline(self):
with self.assertQueryCount(__system__=2, demo=2):
self.assertEqual(self.hr_user.im_status, 'leave_offline')
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_partner_leave_offline(self):
with self.assertQueryCount(__system__=2, demo=2):
self.assertEqual(self.hr_partner.im_status, 'leave_offline')
+1 -1
View File
@@ -246,7 +246,7 @@
</div>
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
<field name="out_of_office_message"/>
</group>
<group name="col_right">
<field name="employee_id" invisible="1"/>
@@ -74,6 +74,22 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, {
getName: function () {
return this._name;
},
/**
* By default, threads do not have out-of-office feature
*
* @return {string|undefined}
*/
getOutOfOfficeInfo: function () {
return undefined;
},
/**
* By default, threads do not have out-of-office feature
*
* @return {string|undefined}
*/
getOutOfOfficeMessage: function () {
return undefined;
},
/**
* Get the status of the thread (e.g. 'online', 'offline', etc.)
*
@@ -1,8 +1,10 @@
odoo.define('mail.model.DMChat', function (require) {
"use strict";
var core = require('web.core');
var TwoUserChannel = require('mail.model.TwoUserChannel');
var core = require('web.core');
var _t = core._t;
/**
@@ -20,7 +22,8 @@ var DMChat = TwoUserChannel.extend({
* @param {integer} params.data.direct_partner[0].id
* @param {string} params.data.direct_partner[0].im_status
* @param {string} params.data.direct_partner[0].name
* @param {string} [params.data.direct_partner[0].out_of_office_message='']
* @param {string} [params.data.direct_partner[0].out_of_office_message]
* @param {string} [params.data.direct_partner[0].out_of_office_date_end]
*/
init: function (params) {
this._super.apply(this, arguments);
@@ -54,14 +57,31 @@ var DMChat = TwoUserChannel.extend({
return this._directPartnerID;
},
/**
* Get the out of office info
*
* @override {mail.model.AbstractThread}
* @returns {string|undefined}
*/
getOutOfOfficeInfo: function () {
if (!this._outOfOfficeDateEnd) {
return undefined;
}
var currentDate = new Date();
var date = new Date(this._outOfOfficeDateEnd);
var options = { day: 'numeric', month: 'short' };
if (currentDate.getFullYear() !== date.getFullYear()) {
options.year = 'numeric';
}
var formattedDate = date.toLocaleDateString(window.navigator.language, options);
return _.str.sprintf(_t("Out of office until %s"), formattedDate);
},
/**
* Get the out of office message of the thread
*
* @override {mail.model.AbstractThread}
* @returns {string}
*/
getOutOfOfficeMessage: function () {
if (this._outOfOfficeMessage === '') {
return undefined;
}
return this._outOfOfficeMessage;
},
/**
@@ -148,17 +148,23 @@ var AbstractThreadWindow = Widget.extend({
return this._thread;
},
/**
* Get out of office user text
*Get out of office info
*
* @returns {string|undefined}
*/
getOutOfOfficeMessage: function () {
if (
!(
this.hasThread() &&
this._thread.getType() === 'dm_chat'
)
) {
getOutOfOfficeInfo: function () {
if (!this.hasThread()) {
return undefined;
}
return this._thread.getOutOfOfficeInfo();
},
/**
* Get out of office user text
*
* @returns {string|undefined}
*/
getOutOfOfficeMessage: function () {
if (!this.hasThread()) {
return undefined;
}
return this._thread.getOutOfOfficeMessage();
@@ -82,6 +82,10 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
.o_out_of_office {
background-color: #9dd0e8;
padding:8px;
.o_out_of_office_info {
font-weight: bold;
}
}
.o_mail_thread {
+3 -2
View File
@@ -6,8 +6,9 @@
-->
<t t-name="mail.ThreadWindow" t-extend="mail.AbstractThreadWindow">
<t t-jquery=".o_thread_window_header" t-operation="after">
<div class="o_out_of_office" t-if="widget.getOutOfOfficeMessage() !== undefined">
<div class="o_out_of_office_text" t-esc="widget.getOutOfOfficeMessage()"/>
<div class="o_out_of_office" t-if="widget.getOutOfOfficeInfo() or widget.getOutOfOfficeMessage()">
<div class="o_out_of_office_info" t-if="widget.getOutOfOfficeInfo()" t-esc="widget.getOutOfOfficeInfo()"/>
<div class="o_out_of_office_text" t-if="widget.getOutOfOfficeMessage()" t-esc="widget.getOutOfOfficeMessage()"/>
</div>
<div t-if="!widget.hasThread()" class="o_thread_search_input">
<span> To: </span>