[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:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user