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: