diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 1f094445a44..656542fa8f2 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -132,16 +132,35 @@ class MailActivity(models.Model): def create(self, values): activity = super(MailActivity, self).create(values) self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), + {'type': 'activity_updated', 'activity_created': True}) return activity @api.multi def write(self, values): res = super(MailActivity, self).write(values) if values.get('user_id'): + pre_responsibles = self.mapped('user_id.partner_id') for activity in self: self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), + {'type': 'activity_updated', 'activity_created': True}) + for partner in pre_responsibles: + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', partner.id), + {'type': 'activity_updated', 'activity_deleted': True}) return res + @api.multi + def unlink(self): + for activity in self: + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), + {'type': 'activity_updated', 'activity_deleted': True}) + return super(MailActivity, self).unlink() + @api.multi def action_done(self, feedback=False): message = self.env['mail.message'] diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 18d73ecb4dd..c81981df52d 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -5,7 +5,7 @@ import logging from email.utils import formataddr -from odoo import _, api, fields, models, SUPERUSER_ID, tools +from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools from odoo.exceptions import UserError, AccessError from odoo.osv import expression from odoo.tools import pycompat @@ -420,6 +420,8 @@ class Message(models.Model): for message in message_values: message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal'] message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] + if message['model']: + message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module) return message_values #------------------------------------------------------ diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 059e0f521ad..a7b704188f1 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import _, api, exceptions, fields, models +from odoo import _, api, exceptions, fields, models, modules from odoo.tools import pycompat @@ -119,6 +119,36 @@ class Users(models.Model): def message_get_suggested_recipients(self): return dict((res_id, list()) for res_id in self._ids) + @api.model + def activity_user_count(self): + query = """SELECT m.name, count(*), act.res_model as model, + CASE + WHEN now()::date - act.date_deadline::date = 0 Then 'today' + WHEN now()::date - act.date_deadline::date > 0 Then 'overdue' + WHEN now()::date - act.date_deadline::date < 0 Then 'planned' + END AS states + FROM mail_activity AS act + JOIN ir_model AS m ON act.res_model_id = m.id + WHERE user_id = %s + GROUP BY m.name, states, act.res_model; + """ + self.env.cr.execute(query, [self.env.uid]) + activity_data = self.env.cr.dictfetchall() + + user_activities = {} + for activity in activity_data: + if not user_activities.get(activity['model']): + user_activities[activity['model']] = { + 'name': activity['name'], + 'model': activity['model'], + 'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module), + 'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0, + } + user_activities[activity['model']]['%s_count' % activity['states']] += activity['count'] + user_activities[activity['model']]['total_count'] += activity['count'] + + return user_activities.values() + class res_groups_mail_channel(models.Model): """ Update of res.groups class diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index f7aaf73637e..ce44841533c 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -137,6 +137,7 @@ function make_message (data) { model: data.model, res_id: data.res_id, url: session.url("/mail/view?message_id=" + data.id), + module_icon:data.module_icon, }; _.each(_.keys(emoji_substitutions), function (key) { @@ -476,6 +477,8 @@ function on_partner_notification (data) { on_channel_seen_notification(data); } else if (data.info === 'transient_message') { on_transient_message_notification(data); + } else if (data.type === 'activity_updated') { + onActivityUpdateNodification(data); } else { on_chat_session_notification(data); } @@ -599,6 +602,9 @@ function on_transient_message_notification (data) { add_message(data); } +function onActivityUpdateNodification (data) { + chat_manager.bus.trigger('activity_updated', data); +} // Public interface //---------------------------------------------------------------------------------- var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { @@ -1128,6 +1134,20 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { get_channels_preview: function (channels) { var channels_preview = _.map(channels, function (channel) { + if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) { + // map inbox(mail_message) data with existing channel/chat template + var info = _.pick(channel, 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', 'subject','date', 'record_name', 'status', 'displayed_author', 'email_from', 'unread_counter'); + info.last_message = { + body: info.body, + date: info.date, + displayed_author: info.displayed_author || info.email_from, + }; + info.name = info.record_name || info.subject || info.displayed_author; + info.image_src = info.module_icon || info.avatar_src; + info.message_id = info.id; + info.id = 'channel_inbox'; + return info; + } var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter'); info.last_message = _.last(channel.cache['[]'].messages); if (!info.is_chat) { diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index e611404b07c..5fd383fee18 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -268,6 +268,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { chat_manager.bus.on('update_starred', self, self.throttled_renderSidebar); chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_renderSidebar); chat_manager.bus.on('update_dm_presence', self, self.throttled_renderSidebar); + chat_manager.bus.on('activity_updated', self, self.throttled_renderSidebar); self.thread.$el.on("scroll", null, _.debounce(function () { if (self.thread.is_at_bottom()) { chat_manager.mark_channel_as_seen(self.channel); diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index cd49e72042b..6297f0da8c4 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -2,6 +2,8 @@ odoo.define('mail.systray', function (require) { "use strict"; var core = require('web.core'); +var framework = require('web.framework'); +var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); @@ -9,39 +11,6 @@ var chat_manager = require('mail.chat_manager'); var QWeb = core.qweb; -/** - * Menu item appended in the systray part of the navbar, redirects to the Inbox in Discuss - * Also displays the needaction counter (= Inbox counter) - */ -var InboxItem = Widget.extend({ - template:'mail.chat.InboxItem', - events: { - "click": "on_click", - }, - start: function () { - this.$needaction_counter = this.$('.o_notification_counter'); - chat_manager.bus.on("update_needaction", this, this.update_counter); - chat_manager.is_ready.then(this.update_counter.bind(this)); - return this._super(); - }, - update_counter: function () { - var counter = chat_manager.get_needaction_counter(); - this.$needaction_counter.text(counter); - this.$el.toggleClass('o_no_notification', !counter); - }, - on_click: function (event) { - event.preventDefault(); - chat_manager.is_ready.then(this.discuss_redirect.bind(this)); - }, - discuss_redirect: _.debounce(function () { - var self = this; - this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}).then(function () { - self.trigger_up('hide_app_switcher'); - core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id()); - }); - }, 1000, true), -}); - /** * Menu item appended in the systray part of the navbar * @@ -56,12 +25,13 @@ var MessagingMenu = Widget.extend({ "click": "on_click", "click .o_filter_button": "on_click_filter_button", "click .o_new_message": "on_click_new_message", - "click .o_mail_channel_preview": "on_click_channel", + "click .o_mail_channel_preview": "_onClickChannel", }, start: function () { this.$filter_buttons = this.$('.o_filter_button'); this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels'); this.filter = false; + chat_manager.bus.on("update_needaction", this, this.update_counter); chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter); chat_manager.is_ready.then(this.update_counter.bind(this)); return this._super(); @@ -70,10 +40,9 @@ var MessagingMenu = Widget.extend({ return this.$el.hasClass('open'); }, update_counter: function () { - var counter = chat_manager.get_unread_conversation_counter(); + var counter = chat_manager.get_needaction_counter() + chat_manager.get_unread_conversation_counter(); this.$('.o_notification_counter').text(counter); this.$el.toggleClass('o_no_notification', !counter); - this.$el.toggleClass('o_unread_chat', !!chat_manager.get_chat_unread_counter()); if (this.is_open()) { this.update_channels_preview(); } @@ -94,8 +63,23 @@ var MessagingMenu = Widget.extend({ return channel.type !== 'static'; } }); - - chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self)); + chat_manager.get_messages({channel_id: 'channel_inbox'}).then(function(result) { + var res = []; + _.each(result, function (message) { + message.unread_counter = 1; + var duplicatedMessage = _.findWhere(res, {model: message.model, 'res_id': message.res_id}); + if (message.model && message.res_id && duplicatedMessage) { + message.unread_counter = duplicatedMessage.unread_counter + 1; + res[_.findIndex(res, duplicatedMessage)] = message; + } else { + res.push(message); + } + }); + if (self.filter === 'channel_inbox' || !self.filter) { + channels = _.union(channels, res); + } + chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self)); + }); }); }, _render_channels_preview: function (channels_preview) { @@ -136,16 +120,172 @@ var MessagingMenu = Widget.extend({ on_click_new_message: function () { chat_manager.bus.trigger('open_chat'); }, - on_click_channel: function (event) { - var channel_id = $(event.currentTarget).data('channel_id'); - var channel = chat_manager.get_channel(channel_id); - if (channel) { - chat_manager.open_channel(channel); + + // Handlers + + /** + * When a channel is clicked on, we want to open chat/channel window + * If channel is inbox then redirect to that record view + * If record not linked redirect to Inbox + * @private + * @param {MouseEvent} event + */ + _onClickChannel: function (event) { + var channelID = $(event.currentTarget).data('channel_id'); + if (channelID == 'channel_inbox') { + var resID = $(event.currentTarget).data('res_id'); + var resModel = $(event.currentTarget).data('res_model'); + if (resModel && resID) { + this.do_action({ + type: 'ir.actions.act_window', + res_model: resModel, + views: [[false, 'form'], [false, 'kanban']], + res_id: resID + }); + } else { + // if no model linked redirect to inbox + framework.redirect('mail/view?message_id=channel_inbox'); + } + } else { + var channel = chat_manager.get_channel(channelID); + if (channel) { + chat_manager.open_channel(channel); + } } }, }); -SystrayMenu.Items.push(MessagingMenu); -SystrayMenu.Items.push(InboxItem); +/** + * Menu item appended in the systray part of the navbar, redirects to the next activities of all app + */ +var ActivityMenu = Widget.extend({ + template:'mail.chat.ActivityMenu', + events: { + "click": "_onActivityMenuClick", + "click .o_activity_filter_button, .o_mail_channel_preview": "_onActivityFilterClick", + }, + start: function () { + this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels'); + chat_manager.bus.on("activity_updated", this, this._updateCounter); + chat_manager.is_ready.then(this._updateCounter.bind(this)); + this._updateActivityPreview(); + return this._super(); + }, + + // Private + + /** + * Make RPC and get current user's activity details + * @private + */ + _getActivityData: function(){ + var self = this; + + return self._rpc({ + model: 'res.users', + method: 'activity_user_count', + }).then(function (data) { + self.isCounterUpdated = false; + self.activities = data; + self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0); + self.$('.o_notification_counter').text(self.activityCounter); + self.$el.toggleClass('o_no_notification', !self.activityCounter); + }); + }, + /** + * Get particular model view to redirect on click of activity scheduled on that model. + * @private + * @param {string} model + */ + _getActivityModelViewID: function (model) { + return this._rpc({ + model: model, + method: 'get_activity_view_id' + }); + }, + /** + * Check wether activity systray dropdown is open or not + * @private + * @returns {boolean} + */ + _isOpen: function () { + return this.$el.hasClass('open'); + }, + /** + * Update(render) activity system tray view on activity updation. + * @private + */ + _updateActivityPreview: function () { + var self = this; + self._getActivityData().then(function (){ + self.$activities_preview.html(QWeb.render('mail.chat.ActivityMenuPreview', { + activities : self.activities + })); + }); + }, + /** + * update counter based on activity status(created or Done) + * @private + * @param {Object} [data] key, value to decide activity created or deleted + * @param {String} [data.type] notification type + * @param {Boolean} [data.activity_deleted] when activity deleted + * @param {Boolean} [data.activity_created] when activity created + */ + _updateCounter: function (data) { + if (data) { + if (data.activity_created) { + this.activityCounter ++; + } + if (data.activity_deleted && this.activityCounter > 0) { + this.activityCounter --; + } + this.isCounterUpdated = true; + this.$('.o_notification_counter').text(this.activityCounter); + this.$el.toggleClass('o_no_notification', !this.activityCounter); + } + }, + + + // Handlers + + /** + * Redirect to particular model view + * @private + * @param {MouseEvent} event + */ + _onActivityFilterClick: function (event) { + event.stopPropagation(); + var $target = $(event.currentTarget); + var context = {}; + context['search_default_activities_' + $target.data('filter')] = 1; + this.do_action({ + type: 'ir.actions.act_window', + name: $target.data('model_name'), + res_model: $target.data('res_model'), + views: [[false, 'kanban'], [false, 'form']], + search_view_id: [false], + domain: [['activity_user_id', '=', session.uid]], + context:context, + }); + }, + /** + * When menu clicked update activity preview if counter updated + * @private + * @param {MouseEvent} event + */ + _onActivityMenuClick: function () { + if (!this._isOpen() && this.isCounterUpdated) { + this._updateActivityPreview(); + } + }, }); + +SystrayMenu.Items.push(MessagingMenu); +SystrayMenu.Items.push(ActivityMenu); + +// to test activity menu in qunit test cases we need it +return { + ActivityMenu: ActivityMenu, +}; +}); diff --git a/addons/mail/static/src/less/systray.less b/addons/mail/static/src/less/systray.less index 3a0c0491da2..9c1d0905a77 100644 --- a/addons/mail/static/src/less/systray.less +++ b/addons/mail/static/src/less/systray.less @@ -12,14 +12,19 @@ display: none; } } - &.o_unread_chat > a > i { - color: @odoo-brand-optional; - } - &.open .o_mail_navbar_dropdown { .o-flex-display(); .o-flex-flow(column, nowrap); } + .o_notification_counter { + background: @odoo-brand-optional; + color: white; + padding: 0em 0.3em; + font-size: 0.7em; + top: 7%; + position: absolute; + margin: 0px 0px 0px 8px; + } .o_mail_navbar_dropdown { width: 350px; padding: 0; @@ -107,12 +112,24 @@ } .o_last_message_preview { width: 100%; + max-height: 20px; color: @odoo-main-color-muted; .o-text-overflow(); } } } } + .o_activity_filter_button { + .btn-link; + padding: 2px; + } + .o_no_activity { + cursor: initial; + .o-align-items(center); + color: grey; + opacity: 0.5; + padding: 3px; + } } } diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index 2897cd15d50..77f356be27f 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -1,18 +1,10 @@ - -
  • - - - -
  • -
    -