From 086405344e7302d9f7b243ba0c30d55131aeb3fe Mon Sep 17 00:00:00 2001 From: XavierDo Date: Mon, 7 Jan 2019 10:56:19 +0000 Subject: [PATCH] [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 --- addons/hr_holidays/models/__init__.py | 5 +- addons/hr_holidays/models/hr_leave.py | 10 +- addons/hr_holidays/models/mail_channel.py | 36 +++++++ addons/hr_holidays/models/res_partner.py | 22 +++++ addons/hr_holidays/models/res_users.py | 29 ++++++ addons/hr_holidays/static/src/xml/discuss.xml | 9 ++ addons/hr_holidays/tests/__init__.py | 1 + .../hr_holidays/tests/test_out_of_office.py | 96 +++++++++++++++++++ addons/hr_holidays/views/hr_leave_views.xml | 2 +- .../src/js/models/threads/abstract_thread.js | 16 ++++ .../static/src/js/models/threads/dm_chat.js | 30 +++++- .../thread_windows/abstract_thread_window.js | 22 +++-- .../src/scss/abstract_thread_window.scss | 4 + addons/mail/static/src/xml/thread_window.xml | 5 +- 14 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 addons/hr_holidays/models/mail_channel.py create mode 100644 addons/hr_holidays/models/res_partner.py create mode 100644 addons/hr_holidays/models/res_users.py create mode 100644 addons/hr_holidays/static/src/xml/discuss.xml create mode 100644 addons/hr_holidays/tests/test_out_of_office.py diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py index d391276412b..2da28fe24fa 100644 --- a/addons/hr_holidays/models/__init__.py +++ b/addons/hr_holidays/models/__init__.py @@ -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 \ No newline at end of file +from . import mail_channel +from . import mail_message_subtype +from . import res_partner +from . import res_users diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index c475dec01c7..5536a80f1b8 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -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 diff --git a/addons/hr_holidays/models/mail_channel.py b/addons/hr_holidays/models/mail_channel.py new file mode 100644 index 00000000000..b6d22625411 --- /dev/null +++ b/addons/hr_holidays/models/mail_channel.py @@ -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 diff --git a/addons/hr_holidays/models/res_partner.py b/addons/hr_holidays/models/res_partner.py new file mode 100644 index 00000000000..dde82f5cc9b --- /dev/null +++ b/addons/hr_holidays/models/res_partner.py @@ -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) diff --git a/addons/hr_holidays/models/res_users.py b/addons/hr_holidays/models/res_users.py new file mode 100644 index 00000000000..3df8f5175f5 --- /dev/null +++ b/addons/hr_holidays/models/res_users.py @@ -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()] diff --git a/addons/hr_holidays/static/src/xml/discuss.xml b/addons/hr_holidays/static/src/xml/discuss.xml new file mode 100644 index 00000000000..33128c54c3f --- /dev/null +++ b/addons/hr_holidays/static/src/xml/discuss.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py index 9510f75b34b..67c8327e999 100644 --- a/addons/hr_holidays/tests/__init__.py +++ b/addons/hr_holidays/tests/__init__.py @@ -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 diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py new file mode 100644 index 00000000000..05e3e480cc0 --- /dev/null +++ b/addons/hr_holidays/tests/test_out_of_office.py @@ -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') diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index f655906d0af..c38c2d9f7dd 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -246,7 +246,7 @@ - + diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js index d6a37967c68..a515c4d2f8a 100644 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ b/addons/mail/static/src/js/models/threads/abstract_thread.js @@ -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.) * diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js index 21fb72e91ae..fb524515193 100644 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -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; }, /** diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js index da958c1c5f1..300bfb11a3e 100644 --- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js +++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js @@ -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(); diff --git a/addons/mail/static/src/scss/abstract_thread_window.scss b/addons/mail/static/src/scss/abstract_thread_window.scss index 62b0c42d033..50db05d952c 100644 --- a/addons/mail/static/src/scss/abstract_thread_window.scss +++ b/addons/mail/static/src/scss/abstract_thread_window.scss @@ -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 { diff --git a/addons/mail/static/src/xml/thread_window.xml b/addons/mail/static/src/xml/thread_window.xml index 4d175fcf2b8..d519d0ebbec 100644 --- a/addons/mail/static/src/xml/thread_window.xml +++ b/addons/mail/static/src/xml/thread_window.xml @@ -6,8 +6,9 @@ --> -
-
+
+
+
To: