diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index 7a7f8b497db..8187e2d43e6 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -69,7 +69,7 @@ class ImBus(models.Model): self.sendmany([[channel, message]]) @api.model - def poll(self, channels, last=0, options=None, force_status=False): + def poll(self, channels, last=0, options=None): if options is None: options = {} # first poll return the notification in the 'buffer' @@ -89,15 +89,6 @@ class ImBus(models.Model): 'channel': json.loads(notif['channel']), 'message': json.loads(notif['message']), }) - - if result or force_status: - partner_ids = options.get('bus_presence_partner_ids') - if partner_ids: - partners = self.env['res.partner'].browse(partner_ids) - result += [{ - 'id': -1, - 'channel': (self._cr.dbname, 'bus.presence'), - 'message': {'id': r.id, 'im_status': r.im_status}} for r in partners] return result @@ -145,7 +136,7 @@ class ImDispatch(object): event.wait(timeout=timeout) with registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) - notifications = env['bus.bus'].poll(channels, last, options, force_status=True) + notifications = env['bus.bus'].poll(channels, last, options) except Exception: # timeout pass diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py index a476e0c08dc..3f67d2d1c9f 100644 --- a/addons/bus/models/res_partner.py +++ b/addons/bus/models/res_partner.py @@ -15,50 +15,16 @@ class ResPartner(models.Model): self.env.cr.execute(""" SELECT U.partner_id as id, - CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' - ELSE 'online' + CASE WHEN max(B.last_poll) IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' + ELSE 'online' END as status FROM bus_presence B - JOIN res_users U ON B.user_id = U.id + RIGHT JOIN res_users U ON B.user_id = U.id WHERE U.partner_id IN %s AND U.active = 't' GROUP BY U.partner_id """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) for partner in self: - partner.im_status = res.get(partner.id, 'offline') - - @api.model - def im_search(self, name, limit=20): - """ Search partner with a name and return its id, name and im_status. - Note : the user must be logged - :param name : the partner name to search - :param limit : the limit of result to return - """ - # This method is supposed to be used only in the context of channel creation or - # extension via an invite. As both of these actions require the 'create' access - # right, we check this specific ACL. - if self.env['mail.channel'].check_access_rights('create', raise_exception=False): - name = '%' + name + '%' - excluded_partner_ids = [self.env.user.partner_id.id] - self.env.cr.execute(""" - SELECT - U.id as user_id, - P.id as id, - P.name as name, - CASE WHEN B.last_poll IS NULL THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' - ELSE 'online' - END as im_status - FROM res_users U - JOIN res_partner P ON P.id = U.partner_id - LEFT JOIN bus_presence B ON B.user_id = U.id - WHERE P.name ILIKE %s - AND P.id NOT IN %s - AND U.active = 't' - LIMIT %s - """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) - return self.env.cr.dictfetchall() - else: - return {} + partner.im_status = res.get(partner.id, 'im_partner') # if not found, it is a partner, useful to avoid to refresh status in js diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index 4764e2e52b0..a62f4897391 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -37,7 +37,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, { // bus presence this._lastPresenceTime = new Date().getTime(); - this._lastPartnersPresenceCheck = this._lastPresenceTime; $(window).on("focus." + this._id, this._onFocusChange.bind(this, {focus: true})); $(window).on("blur." + this._id, this._onFocusChange.bind(this, {focus: false})); $(window).on("unload." + this._id, this._onFocusChange.bind(this, {focus: false})); @@ -177,11 +176,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, { var options = _.extend({}, this._options, { bus_inactivity: now - this._getLastPresence(), }); - if (this._lastPartnersPresenceCheck + this.PARTNERS_PRESENCE_CHECK_PERIOD > now) { - options = _.omit(options, 'bus_presence_partner_ids'); - } else { - this._lastPartnersPresenceCheck = now; - } var data = {channels: this._channels, last: this._lastNotificationID, options: options}; // The backend has a maximum cycle time of 50 seconds so give +10 seconds this._pollRpc = this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000}); diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 04be083d8da..b6f65d79eec 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -7,6 +7,8 @@ import threading from odoo.tools.misc import split_every from odoo import _, api, fields, models, registry, SUPERUSER_ID +from odoo.addons.bus.models.bus_presence import AWAY_TIMER +from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER from odoo.osv import expression _logger = logging.getLogger(__name__) @@ -252,3 +254,38 @@ class Partner(models.Model): partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])] return [users, partners] + + @api.model + def im_search(self, name, limit=20): + """ Search partner with a name and return its id, name and im_status. + Note : the user must be logged + :param name : the partner name to search + :param limit : the limit of result to return + """ + # This method is supposed to be used only in the context of channel creation or + # extension via an invite. As both of these actions require the 'create' access + # right, we check this specific ACL. + if self.env['mail.channel'].check_access_rights('create', raise_exception=False): + name = '%' + name + '%' + excluded_partner_ids = [self.env.user.partner_id.id] + self.env.cr.execute(""" + SELECT + U.id as user_id, + P.id as id, + P.name as name, + CASE WHEN B.last_poll IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' + ELSE 'online' + END as im_status + FROM res_users U + JOIN res_partner P ON P.id = U.partner_id + LEFT JOIN bus_presence B ON B.user_id = U.id + WHERE P.name ILIKE %s + AND P.id NOT IN %s + AND U.active = 't' + LIMIT %s + """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) + return self.env.cr.dictfetchall() + else: + return {} diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 8a045ac2d82..58e334808bf 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -371,7 +371,15 @@ var BasicComposer = Widget.extend({ { limit: limit, search: search } ); } - return suggestions; + return $.when(suggestions).then(function (suggestions) { + //add im_status on suggestions + _.each(suggestions, function (suggestionsSet) { + _.each(suggestionsSet, function (suggestion) { + suggestion.im_status = self.call('mail_service', 'getImStatus', { partnerID: suggestion.id }); + }); + }); + return suggestions; + }); }); }, /** diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 808cd8338ad..073befc5fcd 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -56,7 +56,10 @@ var PartnerInviteDialog = Dialog.extend({ allowClear: true, multiple: true, formatResult: function (item) { - var status = QWeb.render('mail.UserStatus', { status: item.im_status }); + var status = QWeb.render('mail.UserStatus', { + status: self.call('mail_service', 'getImStatus', { partnerID: item.id }), + partnerID: item.id, + }); return $('').text(item.text).prepend(status); }, query: function (query) { @@ -992,10 +995,10 @@ var Discuss = AbstractAction.extend({ .on('new_channel', this, this._onNewChannel) .on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible) .on('unsubscribe_from_channel', this, this._onChannelLeft) + .on('updated_im_status', this, this._onUpdatedImStatus) .on('update_needaction', this, this._onUpdateNeedaction) .on('update_starred', this, this._onUpdateStarred) .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter) - .on('update_dm_presence', this, this._onUpdateDmPresence) .on('activity_updated', this, this._onActivityUpdated) .on('update_moderation_counter', this, this._onUpdateModerationCounter) .on('update_typing_partners', this, this._onTypingPartnersUpdated) @@ -1572,7 +1575,7 @@ var Discuss = AbstractAction.extend({ /** * @private */ - _onUpdateDmPresence: function () { + _onUpdatedImStatus: function () { this._throttledUpdateThreads(); }, /** diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js index c1ae22b9708..bb2a19cc6ac 100644 --- a/addons/mail/static/src/js/models/messages/abstract_message.js +++ b/addons/mail/static/src/js/models/messages/abstract_message.js @@ -75,6 +75,14 @@ var AbstractMessage = Class.extend({ } return this._serverAuthorID[0]; }, + /** + * Threads do not have an im status by default + * + * @return {undefined} + */ + getAuthorImStatus: function () { + return undefined; + }, /** * Get the relative url of the avatar to display next to the message * diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index dc98bb7195b..a414a11874b 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -65,6 +65,16 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin addCustomerEmailData: function (data) { this._customerEmailData.push(data); }, + /** + * @override + * @return {string|undefined} + */ + getAuthorImStatus: function () { + if (!this.hasAuthor()) { + return undefined; + } + return this.call('mail_service', 'getImStatus', { partnerID: this.getAuthorID() }); + }, /** * Get the name of the author of this message * If there are no author, return "". 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 5e69dbeff8c..4967a1e9963 100644 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -26,8 +26,14 @@ var DMChat = TwoUserChannel.extend({ this._directPartnerID = data.direct_partner[0].id; this._name = data.custom_channel_name || data.direct_partner[0].name; - this._status = data.direct_partner[0].im_status; + this._outOfOfficeMessage = data.direct_partner[0].out_of_office_message; + this._outOfOfficeDateEnd = data.direct_partner[0].out_of_office_date_end; this._type = 'dm_chat'; + + this.call('mail_service', 'updateImStatus', [{ + id: this._directPartnerID, + im_status: data.direct_partner[0].im_status + }]); }, //-------------------------------------------------------------------------- @@ -53,10 +59,11 @@ var DMChat = TwoUserChannel.extend({ return result; }, /** - * @param {string} newStatus + * @override + * @return {string} */ - setStatus: function (newStatus) { - this._status = newStatus; + getStatus: function () { + return this.call('mail_service', 'getImStatus', { partnerID: this._directPartnerID }); }, }); diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 777df1df4a6..204a88bb8e9 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -443,10 +443,6 @@ var MailManager = AbstractService.extend({ channel = this._makeChannel(data, options); if (channel.getType() === 'dm_chat') { this._pinnedDmPartners.push(channel.getDirectPartnerID()); - this.call('bus_service', 'updateOption', - 'bus_presence_partner_ids', - this._pinnedDmPartners - ); } this._threads.push(channel); if (data.last_message) { @@ -1041,10 +1037,6 @@ var MailManager = AbstractService.extend({ var index = this._pinnedDmPartners.indexOf(channel.getDirectPartnerID()); if (index > -1) { this._pinnedDmPartners.splice(index, 1); - this.call('bus_service', 'updateOption', - 'bus_presence_partner_ids', - this._pinnedDmPartners - ); } } this._threads = _.without(this._threads, channel); diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index b59d7b91434..b77fa3e65fd 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -493,22 +493,6 @@ MailManager.include({ self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID); }); }, - /** - * On receiving an update on user status (e.g. becoming 'online', 'offline', - * 'idle', etc.). - * - * @private - * @param {Object} data partner infos - * @param {integer} data.id partner ID - * @param {string} data.im_status partner new 'im status' (e.g. 'online') - */ - _handlePresenceNotification: function (data) { - var dmChat = this.getDMChatFromPartnerID(data.id); - if (dmChat) { - dmChat.setStatus(data.im_status); - this._mailBus.trigger('update_dm_presence', dmChat); - } - }, /** * @override * @private @@ -569,8 +553,6 @@ MailManager.include({ }); } else if (model === 'res.partner') { self._handlePartnerNotification(notif[1]); - } else if (model === 'bus.presence') { - self._handlePresenceNotification(notif[1]); } }); }, diff --git a/addons/mail/static/src/js/services/mail_service.js b/addons/mail/static/src/js/services/mail_service.js index 4f6b0c29dfc..2256e696c2f 100644 --- a/addons/mail/static/src/js/services/mail_service.js +++ b/addons/mail/static/src/js/services/mail_service.js @@ -5,6 +5,7 @@ var core = require('web.core'); var MailManager = require('mail.Manager'); +require('mail.Manager.Status'); require('mail.Manager.Notification'); require('mail.Manager.Window'); require('mail.Manager.DocumentThread'); diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js new file mode 100644 index 00000000000..2b75cdc6ee7 --- /dev/null +++ b/addons/mail/static/src/js/services/mail_status_manager.js @@ -0,0 +1,216 @@ +odoo.define('mail.Manager.Status', function (require) { +"use strict"; + +var core = require('web.core'); +var MailManager = require('mail.Manager'); +var QWeb = core.qweb; + +/** + * Mail Manager: IM Status + * + * This component handles im status of partners, which is useful for DM Chats, + * partner mention suggestions, and chatter messages that display the user icon. + */ +MailManager.include({ + _UPDATE_INTERVAL: 50, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the uin cache im status, and triggers an asynchronous throttled + * update of im_status cache for missings + * + * @param {Object} data + * @param {integer} data.partnerID + * @return {String} + */ + getImStatus: function (data) { + var self = this; + var partnerID = data.partnerID; + if (!this._imStatus[partnerID]) { + // Add to list to call it in next bus update or _fetchMissingImStatus + this._imStatus[partnerID] = undefined; + // fetch after some time if no other getImStatus occurs + this._clearStatusServiceTimeout(this._fetchStatusTimeout); + this._fetchStatusTimeout = this._setStatusServiceTimeout(function () { + self._fetchMissingImStatus(); + }, 500); + } + return this._imStatus[partnerID]; + }, + /** + * Update status manually, to avoid to do a rpc and an asynchronous update + * after getImStatus. Can be done by any caller knowing the last im_status + * state. + * + * @param {Object[]} statusList, A list of {id, im_status} + */ + updateImStatus: function (statusList) { + var updatedIDs = []; + var self = this; + _.each(statusList, function (status) { + if (self._imStatus[status.id] === status.im_status) { + return; + } + updatedIDs.push(status.id); + self._imStatus[status.id] = status.im_status; + }); + if (! _.isEmpty(updatedIDs)) { + this._mailBus.trigger('updated_im_status', updatedIDs); // useful for thread window header + this._renderImStatus(updatedIDs); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * A simple clearTimeout, useful for test + * + * @private + * @param {integer} ids + */ + _clearStatusServiceTimeout: function (id) { + clearTimeout(id); + }, + /** + * Fetch the list of im_status for partner with id in ids list and triggers + * an update. + * + * @private + * @param {Object} data + * @param {integer[]} data.partnerIDs + * @return {Deferred} + */ + _fetchImStatus: function (data) { + var self = this; + var partnerIDs = data.partnerIDs; + if (_.isEmpty(partnerIDs)) { + return $.when(); + } + return this._rpc({ + model: 'res.partner', + method: 'read', + args: [partnerIDs, ['id', 'im_status']], + }).then( function (results) { + self.updateImStatus(results); + }); + }, + /** + * Fetch the list of im_status for partner with an unknown im_status and + * triggers an update. + * + * @private + * @return {Deferred} + */ + _fetchMissingImStatus: function () { + var missing = []; + _.each(this._imStatus, function (value, key) { + if (value === undefined) { + missing.push(Number(key)); + } + }); + return this._fetchImStatus({ partnerIDs: missing }); + }, + /** + * @private + * @return {integer[]} a list of partner ids that needs update + */ + _getImStatusToUpdate: function () { + var toUpdate = []; + _.each(this._imStatus, function (status, key) { + //filter on im_partner and bot: useless to update them, status won't change + if (['im_partner', 'bot'].indexOf(status) === -1) { + toUpdate.push(Number(key)); + } + }); + return toUpdate; + }, + /** + * @override + * @private + */ + _initializeInternalState: function () { + this._super.apply(this, arguments); + this._fetchStatusTimeout = undefined; + this._imStatus = {}; + this._isTabFocused = true; + this._updateImStatusLoop(); + }, + /** + * @override + * @private + */ + _listenOnBuses: function () { + this._super.apply(this, arguments); + $(window).on("focus", this._onWindowFocusChange.bind(this, true)); + $(window).on("blur", this._onWindowFocusChange.bind(this, false)); + $(window).on("unload", this._onWindowFocusChange.bind(this, false)); + }, + /** + * @private + * @param {integer[]} updatedIds + */ + _renderImStatus: function (updatedIds) { + var self = this; + $('.o_updatable_im_status').each(function () { + var $this = $(this); + var partnerID = $this.data('partner-id'); + if (partnerID !== undefined && updatedIds.indexOf(partnerID) !== -1) { // todo instead add id on o_updatable_im_status and select only concerned ones + var status = QWeb.render('mail.UserStatus', { + status: self.getImStatus({ partnerID: partnerID }), + partnerID: partnerID, + }); + $this.replaceWith(status); + } + }); + }, + /** + * A simple setTimeout, useful for test + * + * @private + * @param {function} func + * @param {integer} duration + * @return {integer} + */ + _setStatusServiceTimeout: function (func, duration) { + return setTimeout(func, duration); + }, + /** + * Once initialised, this loop will update the im_status of registered + * users. + * + * @private + * @param {integer} [counter=0] The recursion loop counter + */ + _updateImStatusLoop: function (counter) { + var self = this; + if (!_.isNumber(counter)) { + counter = 0; + } + this._setStatusServiceTimeout(function () { + if (counter >= self._UPDATE_INTERVAL && self._isTabFocused) { + self._fetchImStatus({ partnerIDs: self._getImStatusToUpdate() }); + counter = 0; + } + self._updateImStatusLoop(counter+1); + }, 1000); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {boolean} focused + */ + _onWindowFocusChange: function (focused) { + this._isTabFocused = focused; + }, +}); + +}); diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 4971e7f3f4b..e047c71ab89 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -334,8 +334,8 @@ MailManager.include({ .on('new_channel', this, this._onNewChannel) .on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible) .on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel) - .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter) - .on('update_dm_presence', this, this._onUpdateDmPresence); + .on('updated_im_status', this, this._onUpdatedImStatus) + .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter); core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100)); }, @@ -663,6 +663,28 @@ MailManager.include({ _onUnsubscribeFromChannel: function (channelID) { this._closeThreadWindow(channelID); }, + /** + * Called when there is a change of the im status of the partner. + * The header of the thread window should be updated accordingly, + * in order to display the correct new im status of this users. + * + * @private + * @param {integer} partnerID + */ + _onUpdatedImStatus: function (partnerIDs) { + var self = this; + _.each(partnerIDs, function (partnerID) { + var thread = self.getDMChatFromPartnerID(partnerID); + if (! thread) { + return; + } + var threadWindow = self._getThreadWindow(thread.getID()); + if (!threadWindow) { + return; + } + threadWindow.renderHeader(); + }); + }, /** * Called when a thread has its unread counter that has changed. * The unread counter on the thread windows should be updated. @@ -690,21 +712,6 @@ MailManager.include({ this._repositionHiddenWindowsDropdown(); } }, - /** - * Called when there is a change of the im status of the user linked to - * DMs. The header of the thread window should be updated accordingly, - * in order to display the correct new im status of this users. - * - * @private - * @param {mail.model.Thread} thread - */ - _onUpdateDmPresence: function (thread) { - _.each(this._threadWindows, function (threadWindow) { - if (thread.getID() === threadWindow.getID()) { - threadWindow.renderHeader(); - } - }); - }, /** * Called when a message has been updated. * diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss index 096c527aa16..1f2fffcee60 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -55,6 +55,9 @@ font-style: italic; color: gray; } + .fa { + font-size: 0.8em; + } } textarea { resize: none; diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index ff4680d7851..b9de0194e93 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -71,6 +71,10 @@ + + + + () diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index 98430c81ce1..10fb71a5110 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -154,6 +154,7 @@ + # @@ -176,11 +177,14 @@ - - - + + + + + + @@ -129,8 +130,10 @@ - + + + diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py index c715adccc49..2a6ec5eca55 100644 --- a/addons/mail_bot/models/res_partner.py +++ b/addons/mail_bot/models/res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models + class Partner(models.Model): _inherit = 'res.partner' diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml index e7f0650c867..95526e6b11a 100644 --- a/addons/mail_bot/views/discuss.xml +++ b/addons/mail_bot/views/discuss.xml @@ -1,10 +1,5 @@ - - - - -