diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 105d149fa43..4a23ef2573c 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -38,6 +38,7 @@ ], 'demo': [ 'data/mail_demo.xml', + 'data/mail_activity_demo.xml', 'data/mail_channel_demo.xml', ], 'installable': True, diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index f0990c7de40..9682cbe1125 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -230,3 +230,15 @@ class MailController(http.Controller): 'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mail_channel_menu_root_chat'), } return values + + @http.route('/mail/activity/new', type='json', auth='user') + def mail_activity_new(self, note, activity_type_id=None, date_deadline=None): + values = {'note': note} + if date_deadline: + values['date_deadline'] = date_deadline + if not activity_type_id: + activity_type_id = request.env['mail.activity.type'].sudo().search([('category', '=', 'reminder')]).id + if activity_type_id: + values['activity_type_id'] = activity_type_id + activity = request.env['mail.activity'].create(values) + return activity.id diff --git a/addons/mail/data/mail_activity_data.xml b/addons/mail/data/mail_activity_data.xml index 281d1915141..79f01eeaf02 100644 --- a/addons/mail/data/mail_activity_data.xml +++ b/addons/mail/data/mail_activity_data.xml @@ -19,5 +19,11 @@ fa-tasks 5 + + Reminder + reminder + fa-tasks + 0 + diff --git a/addons/mail/data/mail_activity_demo.xml b/addons/mail/data/mail_activity_demo.xml new file mode 100644 index 00000000000..f15a84331d4 --- /dev/null +++ b/addons/mail/data/mail_activity_demo.xml @@ -0,0 +1,10 @@ + + + + +

Lunch with colleagues this Sunday.

+ + +
+
+
\ No newline at end of file diff --git a/addons/mail/data/mail_demo.xml b/addons/mail/data/mail_demo.xml index 0aa8d664ab8..b4e7f2c092e 100644 --- a/addons/mail/data/mail_demo.xml +++ b/addons/mail/data/mail_demo.xml @@ -220,6 +220,5 @@ virginie@agrolait.fr - diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index c08866fa6a4..1259e7395f8 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -5,6 +5,7 @@ from datetime import date, datetime, timedelta from odoo import api, exceptions, fields, models, _ from odoo.osv import expression +from odoo.tools import html2plaintext class MailActivityType(models.Model): @@ -45,7 +46,7 @@ class MailActivityType(models.Model): 'mail.activity.type', 'mail_activity_rel', 'recommended_id', 'activity_id', string='Preceding Activities') category = fields.Selection([ - ('default', 'Other')], default='default', + ('default', 'Other'), ('reminder', 'Reminder')], default='default', string='Category', help='Categories may trigger specific behavior like opening calendar view') @@ -69,10 +70,10 @@ class MailActivity(models.Model): return res # owner - res_id = fields.Integer('Related Document ID', index=True, required=True) + res_id = fields.Integer('Related Document ID', index=True) res_model_id = fields.Many2one( 'ir.model', 'Document Model', - index=True, ondelete='cascade', required=True) + index=True, ondelete='cascade') res_model = fields.Char( 'Related Document Model', index=True, related='res_model_id.model', store=True, readonly=True) @@ -89,6 +90,9 @@ class MailActivity(models.Model): note = fields.Html('Note') feedback = fields.Html('Feedback') date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) + active = fields.Boolean( + 'Open', default=True, + help='Done reminders should be archived instead of marked as done.') # description user_id = fields.Many2one( 'res.users', 'Assigned to', @@ -115,7 +119,8 @@ class MailActivity(models.Model): @api.depends('res_model', 'res_id') def _compute_res_name(self): for activity in self: - activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1] + if activity.res_model and activity.res_id: + activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1] @api.depends('date_deadline') def _compute_state(self): @@ -162,11 +167,17 @@ class MailActivity(models.Model): pass else: return - doc_operation = 'read' if operation == 'read' else 'write' activity_to_documents = dict() for activity in self.sudo(): - activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) + if activity.res_model and activity.res_id: + activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) + else: + if (operation != 'create' and self.env.user.id != activity.user_id.id): + raise exceptions.AccessError(_("You can only access your own records.")) + elif operation == 'create' and not self.env.user.has_group('base.group_user'): + raise exceptions.AccessError(_("Only employee can create reminder.")) + for model, res_ids in activity_to_documents.items(): self.env[model].check_access_rights(doc_operation, raise_exception=True) try: @@ -182,11 +193,19 @@ class MailActivity(models.Model): values_w_defaults = self.default_get(self._fields.keys()) values_w_defaults.update(values) + # Reminder have no summary (for display name) It will add first line of note to summary. + if 'res_model_id' not in values and 'summary' not in values: + values_w_defaults['summary'] = self._compute_summary_from_note(values.get('note', _('Reminder'))) + # continue as sudo because activities are somewhat protected activity = super(MailActivity, self.sudo()).create(values_w_defaults) activity_user = activity.sudo(self.env.user) activity_user._check_access('create') - self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id]) + + # subscribe to document if any + if activity.res_id and activity.res_model: + self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id]) + if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), @@ -196,13 +215,28 @@ class MailActivity(models.Model): @api.multi def write(self, values): self._check_access('write') + if not self.env.user._is_admin() and any(field in values.keys() for field in ['res_model', 'res_id', 'res_model_id']): + raise exceptions.AccessError(_("You cannot re-attach a reminder to another record.")) + if values.get('user_id'): pre_responsibles = self.mapped('user_id.partner_id') - res = super(MailActivity, self.sudo()).write(values) + + reminders = self.filtered(lambda a: not a.res_id and not a.res_model) if 'note' in values and 'summary' not in values else self.env['mail.activity'] + activities = self - reminders + res = True + if activities: + res &= super(MailActivity, activities.sudo()).write(values) + if reminders: # if we have reminder, note is set and summary is not set. We need to update summary + upd_values = dict( + values, + summary=self._compute_summary_from_note(values['note']) + ) + res &= super(MailActivity, reminders.sudo()).write(upd_values) if values.get('user_id'): for activity in self: - self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) + if activity.res_id and activity.res_model: # subscribe to document if any + self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), @@ -225,6 +259,29 @@ class MailActivity(models.Model): {'type': 'activity_updated', 'activity_deleted': True}) return super(MailActivity, self.sudo()).unlink() + @api.multi + def toggle_active(self): + """ Override model-method to send bus notification about (un)archived reminders """ + res = super(MailActivity, self).toggle_active() + for activity in self: + if activity.active: + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), + {'type': 'activity_updated', 'activity_created': True}) + else: + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), + {'type': 'activity_updated', 'activity_deleted': True}) + return res + + def _compute_summary_from_note(self, note): + """ Returns the first line of html note """ + if note: + summary = html2plaintext(note).strip().replace('*', '').split("\n")[0] + else: + summary = _('Reminder') + return summary or _('Reminder') + @api.multi def action_done(self): """ Wrapper without feedback because web button add context as diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index cc6786cce1e..5b7c82d6725 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -70,15 +70,16 @@ class Users(models.Model): @api.model def activity_user_count(self): - query = """SELECT m.id, count(*), act.res_model as model, + # Reminders don't having any model so here we assign 'mail.activity' as reminder's model. + query = """SELECT m.id, 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 + LEFT OUTER JOIN ir_model AS m ON act.res_model_id = m.id + WHERE user_id = %s AND active = TRUE GROUP BY m.id, states, act.res_model; """ self.env.cr.execute(query, [self.env.uid]) @@ -86,7 +87,18 @@ class Users(models.Model): model_ids = [a['id'] for a in activity_data] model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()} - user_activities = {} + # always add default entry for reminders + user_activities = { + None: { + 'name': _("Reminder"), + 'model': None, + 'icon': '/mail/static/src/img/reminder.png', + 'total_count': 0, + 'today_count': 0, + 'overdue_count': 0, + 'planned_count': 0 + } + } for activity in activity_data: if not user_activities.get(activity['model']): user_activities[activity['model']] = { @@ -96,7 +108,7 @@ class Users(models.Model): 'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0, } user_activities[activity['model']]['%s_count' % activity['states']] += activity['count'] - if activity['states'] in ('today','overdue'): + if activity['states'] in ('today', 'overdue'): user_activities[activity['model']]['total_count'] += activity['count'] return list(user_activities.values()) diff --git a/addons/mail/static/src/img/reminder.png b/addons/mail/static/src/img/reminder.png new file mode 100644 index 00000000000..a585b8d6e09 Binary files /dev/null and b/addons/mail/static/src/img/reminder.png differ diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index f6c19a1c164..d382745df29 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -3,11 +3,13 @@ odoo.define('mail.systray', function (require) { var config = require('web.config'); var core = require('web.core'); +var datepicker = require('web.datepicker'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var QWeb = core.qweb; +var _t = core._t; /** * Menu item appended in the systray part of the navbar @@ -157,8 +159,13 @@ var MessagingMenu = Widget.extend({ var ActivityMenu = Widget.extend({ template:'mail.chat.ActivityMenu', events: { - "click": "_onActivityMenuClick", - "click .o_mail_channel_preview": "_onActivityFilterClick", + 'click': '_onActivityMenuClick', + 'click .o_mail_channel_preview:not(.o_reminder)': '_onActivityFilterClick', + 'click .o_reminder_show': '_onAddReminderClick', + 'click .o_reminder_save': '_onReminderSaveClick', + 'click .o_reminder_set_datetime': '_onReminderDateTimeSetClick', + 'keydown input.o_reminder_input': '_onReminderInputKeyDown', + 'click .o_reminder': '_onNewReminderClick', }, start: function () { this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels'); @@ -167,9 +174,9 @@ var ActivityMenu = Widget.extend({ this._updateActivityPreview(); return this._super(); }, - + //-------------------------------------------------- // Private - + //-------------------------------------------------- /** * Make RPC and get current user's activity details * @private @@ -184,8 +191,21 @@ var ActivityMenu = Widget.extend({ context: session.user_context, }, }).then(function (data) { + // We required reminder to be first always. Moving its position from n-th to zero if not at zero + var reminderIndex = _.findIndex(data, function (val) { return val.model == null; }); + if (reminderIndex > 0) { + data.splice(0, 0, data.splice(reminderIndex, 1)[0]); + } self.activities = data; - self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0); + + //we want to add count in reminders title + var reminders = _.findWhere(data, {'model': null}); + if (reminders) { + var totalReminders = reminders.overdue_count + reminders.planned_count + reminders.today_count; + //manualy count total reminders since reminders.total_count only sum today and overdue + reminders.name = _.str.sprintf(_t('Reminders (%s)'), totalReminders); + } + self.activityCounter = _.reduce(self.activities, 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); }); @@ -241,10 +261,30 @@ var ActivityMenu = Widget.extend({ this.$el.toggleClass('o_no_notification', !this.activityCounter); } }, - - + /** + * Save the reminder to database using datepicker date and field as note + * @private + */ + _saveReminder: function () { + var note = this.$('.o_reminder_input').val().trim(); + if (! note) { + return; + } + var params = {'note': note}; + var reminderDateTime = this.reminderDateTimeWidget.getValue(); + if (reminderDateTime) { + params = _.extend(params, {'date_deadline': reminderDateTime}); + } + this.$('.o_reminder_show').removeClass('hidden'); + this.$('.o_reminder').addClass('hidden'); + this._rpc({ + route: '/mail/activity/new', + params: params, + }).then(this._updateActivityPreview.bind(this)); + }, + //------------------------------------------------------------ // Handlers - + //----------------------------------------------------------- /** * Redirect to particular model view * @private @@ -260,15 +300,20 @@ var ActivityMenu = Widget.extend({ } else { context['search_default_activities_' + data.filter] = 1; } - this.do_action({ - type: 'ir.actions.act_window', - name: data.model_name, - res_model: data.res_model, - views: [[false, 'kanban'], [false, 'form']], - search_view_id: [false], - domain: [['activity_user_id', '=', session.uid]], - context:context, - }); + if (data.res_model == null) { + // Opening Reminder custom kanban view with action. + this.do_action('mail.mail_activity_action_reminders', {additional_context: context}); + } else { + this.do_action({ + type: 'ir.actions.act_window', + name: data.model_name, + res_model: 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 @@ -280,7 +325,65 @@ var ActivityMenu = Widget.extend({ this._updateActivityPreview(); } }, - + /** + * When add new reminder button clicked, toggling quick reminder create view inside + * Systray activity view + * + * @private + * @param {MouseEvent} event + */ + _onAddReminderClick: function (event) { + event.stopPropagation(); + if (!this.reminderDateTimeWidget){ + this.reminderDateTimeWidget = new datepicker.DateWidget(this, {useCurrent: true}); + } + this.reminderDateTimeWidget.appendTo(this.$('.o_reminder_datetime')); + this.$('.o_reminder_show, .o_reminder').toggleClass('hidden'); + this.$('.o_reminder_input').val('').focus(); + + }, + /** + * When focusing on input for new quick reminder systerm tray must be open. + * Preventing to close + * + * @private + * @param {MouseEvent} event + */ + _onNewReminderClick: function (event) { + event.stopPropagation(); + }, + /** + * Opens datetime picker for reminder. + * Quick FIX due to no option for set custom icon instead of caret in datepicker. + * + * @private + * @param {MouseEvent} event + */ + _onReminderDateTimeSetClick: function (event) { + event.preventDefault(); + event.stopPropagation(); + this.reminderDateTimeWidget.$input.click(); + }, + /** + * Saving reminder (quick create) and updating activity preview + * + * @private + * @param {MouseEvent} event + */ + _onReminderSaveClick: function (event) { + this._saveReminder(); + }, + /** + * Handling Enter key for quick create reminder. + * + * @private + * @param {KeyboardEvent} event + */ + _onReminderInputKeyDown: function (event) { + if (event.which === $.ui.keyCode.ENTER) { + this._saveReminder(); + } + }, }); SystrayMenu.Items.push(MessagingMenu); diff --git a/addons/mail/static/src/less/discuss.less b/addons/mail/static/src/less/discuss.less index 4e0ae26dccc..b2bfc6236c8 100644 --- a/addons/mail/static/src/less/discuss.less +++ b/addons/mail/static/src/less/discuss.less @@ -391,7 +391,7 @@ .o_channel_title { .o-flex-display(); .o_channel_name { - .o-flex(0, 1, auto); + .o-flex(1, 1, auto); .o-text-overflow(); } .o_channel_counter { @@ -428,6 +428,27 @@ } } +.o_reminder.o_mail_channel_preview { + background-color: white; + .o_reminder_input { + border: none; + } + .o_reminder_datetime { + .o_datepicker { + .o_datepicker_input { + float: right; + text-align: right; + border: none; + font-size: 11px; + width: 50%; + } + .o_datepicker_button { + display: none; + } + } + } +} + @media (max-width: @screen-xs-max) { @o-mail-chatter-mobile-gap: 2%; diff --git a/addons/mail/static/src/less/mail_activity.less b/addons/mail/static/src/less/mail_activity.less index c9fd7c8595c..f98e0974b7b 100644 --- a/addons/mail/static/src/less/mail_activity.less +++ b/addons/mail/static/src/less/mail_activity.less @@ -132,7 +132,15 @@ .o_kanban_inline_block { display: inline-block; } - + .o_text_line_through { + text-decoration: line-through; + } + .o_reminder_title { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding-bottom: 10px; + } .o_mail_activity { .o_activity_btn { span.fa { diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index dd61c486b9b..e70d36ed4c1 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -80,6 +80,29 @@ + +