[IMP] mail, calendar: add today meetings in systray
Purpose of this commit is to display today meetings in the systray. This will help people in their daily job to have quickly access to their meetings. It is displayed along with activities and reminders to have all current work to do in the same place. Displayed meetings are ongoing or not yet started ones of the current day. All-day meetings of the current day are also shown. Done meetings are not displayed to lessen noise. Clicking on the systray will open the calendar view of meetings in a day mode. This is done by a small modification in the calendar view allowing to choose the display mode from context in addition to the mode attribute of the action definition. This commit is linked to task ID 60593. Closes #24438. Thanks to @ged-odoo for quick reviewing.
This commit is contained in:
committed by
Thibault Delavallée
parent
642970494e
commit
4a59f93d15
@@ -7,3 +7,4 @@ from . import res_partner
|
||||
from . import mail_message
|
||||
from . import calendar
|
||||
from . import mail_activity
|
||||
from . import res_users
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models, modules, _
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def _systray_get_calendar_event_domain(self):
|
||||
start_dt = datetime.datetime.utcnow()
|
||||
start_date = datetime.date.today()
|
||||
end_dt = datetime.datetime.combine(start_date, datetime.time.max)
|
||||
|
||||
return ['&', '|',
|
||||
'&',
|
||||
'|', ['start', '>=', fields.Datetime.to_string(start_dt)], ['stop', '>=', fields.Datetime.to_string(start_dt)],
|
||||
['start', '<=', fields.Datetime.to_string(end_dt)],
|
||||
'&', ['allday', '=', True], ['start_date', '=', fields.Date.to_string(start_date)],
|
||||
'&', ('attendee_ids.partner_id', '=', self.env.user.partner_id.id), ('attendee_ids.state', '!=', 'declined')]
|
||||
|
||||
@api.model
|
||||
def systray_get_activities(self):
|
||||
res = super(Users, self).systray_get_activities()
|
||||
|
||||
meetings_lines = self.env['calendar.event'].search_read(
|
||||
self._systray_get_calendar_event_domain(),
|
||||
['id', 'start', 'name', 'allday'],
|
||||
order='start')
|
||||
if meetings_lines:
|
||||
meeting_label = _("Today's Meetings")
|
||||
meetings_systray = {
|
||||
'type': 'meeting',
|
||||
'name': meeting_label,
|
||||
'model': 'calendar.event',
|
||||
'icon': modules.module.get_module_icon(self.env['calendar.event']._original_module),
|
||||
'meetings': meetings_lines,
|
||||
}
|
||||
res.insert(0, meetings_systray)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,79 @@
|
||||
odoo.define('calendar.systray', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ActivityMenu = require('mail.systray').ActivityMenu;
|
||||
var fieldUtils = require('web.field_utils');
|
||||
|
||||
ActivityMenu.include({
|
||||
events: _.extend({}, ActivityMenu.prototype.events, {
|
||||
'click .o_meeting_filter': '_onMeetingFilterClick'
|
||||
}),
|
||||
|
||||
//-----------------------------------------
|
||||
// Private
|
||||
//-----------------------------------------
|
||||
|
||||
/**
|
||||
* parse date to server value
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_getActivityData: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var meeting = _.find(self.activities, {type: 'meeting'});
|
||||
if (meeting && meeting.meetings) {
|
||||
_.each(meeting.meetings, function (res) {
|
||||
res.start = fieldUtils.parse.datetime(res.start, false, {isUTC: true});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//-----------------------------------------
|
||||
// Handlers
|
||||
//-----------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_onActivityFilterClick: function (ev) {
|
||||
var $el = $(ev.currentTarget);
|
||||
var data = _.extend({}, $el.data());
|
||||
if (data.res_model === "calendar.event" && data.filter === "my") {
|
||||
this.do_action('calendar.action_calendar_event', {
|
||||
additional_context: {
|
||||
default_mode: 'day',
|
||||
search_default_mymeetings: 1,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When particular meeting clicked, open particular meeting in form view
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onMeetingFilterClick: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var $el = $(ev.currentTarget);
|
||||
var data = _.extend({}, $el.data());
|
||||
if (data.res_model === "calendar.event") {
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
name: data.model_name,
|
||||
res_model: data.res_model,
|
||||
res_id: data.res_id,
|
||||
views: [[false, 'form'], [false, 'calendar'], [false, 'list']],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -77,3 +77,16 @@
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
.o_meeting_filter {
|
||||
@include text-overflow();
|
||||
width: 64%;
|
||||
color: grey;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
&:hover {
|
||||
color: $o-brand-optional;
|
||||
}
|
||||
&.o_meeting_bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,29 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-extend="mail.chat.ActivityMenuPreview">
|
||||
<t t-jquery="div.o_channel_title" t-operation="after">
|
||||
<div t-if="activity and activity.type == 'meeting'">
|
||||
<t t-set="is_next_meeting" t-value="true"/>
|
||||
<t t-foreach="activity.meetings" t-as="meeting">
|
||||
<div>
|
||||
<span t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activity.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activity.name" t-att-title="meeting.name">
|
||||
<span t-if="!meeting.allday">
|
||||
<t t-esc="moment(meeting.start).local().format('hh:mm A')"/>
|
||||
</span>
|
||||
<span><t t-esc="meeting.name"/></span>
|
||||
</span>
|
||||
<span t-if="meeting.start" class="pull-right">
|
||||
<t t-if="meeting.allday">All Day</t>
|
||||
<t t-else=''>
|
||||
<t t-set="is_next_meeting" t-value="false"/>
|
||||
<t t-esc="moment(meeting.start).fromNow()"/>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
odoo.define('calendar.systray_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ChatManager = require('mail.ChatManager');
|
||||
var systray = require('mail.systray');
|
||||
var testUtils = require('web.test_utils');
|
||||
var createBusService = require('mail.testUtils').createBusService;
|
||||
|
||||
|
||||
QUnit.module('calendar', {}, function () {
|
||||
|
||||
QUnit.module('ActivityMenu', {
|
||||
beforeEach: function () {
|
||||
this.services = [ChatManager, createBusService()];
|
||||
this.data = {
|
||||
'calendar.event': {
|
||||
records: [{
|
||||
name: "Today's meeting (3)",
|
||||
model: "calendar.event",
|
||||
type: 'meeting',
|
||||
meetings: [{
|
||||
id: 1,
|
||||
res_model: "calendar.event",
|
||||
name: "meeting1",
|
||||
start: "2018-04-20 06:30:00",
|
||||
allday: false,
|
||||
},{
|
||||
id: 2,
|
||||
res_model: "calendar.event",
|
||||
name: "meeting2",
|
||||
start: "2018-04-20 09:30:00",
|
||||
allday: false,
|
||||
}]
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('activity menu widget:today meetings', function (assert) {
|
||||
assert.expect(8);
|
||||
var self = this;
|
||||
var activityMenu = new systray.ActivityMenu();
|
||||
testUtils.addMockEnvironment(activityMenu, {
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'systray_get_activities') {
|
||||
return $.when(self.data['calendar.event']['records']);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
activityMenu.appendTo($('#qunit-fixture'));
|
||||
|
||||
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
|
||||
|
||||
testUtils.intercept(activityMenu, 'do_action', function (event) {
|
||||
assert.ok(true, "should have triggered a do_action");
|
||||
assert.strictEqual(event.data.action.res_id, activityMenu.$('.o_meeting_filter').eq(0).data('res_id'), 'should open particular meeting form view')
|
||||
});
|
||||
activityMenu.$('.dropdown-toggle').click();
|
||||
activityMenu.$('.o_meeting_filter').eq(0).click();
|
||||
|
||||
testUtils.intercept(activityMenu, 'do_action', function (event) {
|
||||
assert.strictEqual(event.data.action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode')
|
||||
});
|
||||
activityMenu.$('.o_mail_channel_preview').click();
|
||||
|
||||
assert.ok(activityMenu.$('.o_meeting_filter'), "should be a meeting");
|
||||
assert.strictEqual(activityMenu.$('.o_meeting_filter').length, 2, 'there should be 2 meetings');
|
||||
assert.strictEqual(activityMenu.$('.o_meeting_filter').eq(0).hasClass('o_meeting_bold'), true, 'this meeting is yet to start');
|
||||
assert.strictEqual(activityMenu.$('.o_meeting_filter').eq(1).hasClass('o_meeting_bold'), false, 'this meeting has been started');
|
||||
activityMenu.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,12 +5,14 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/calendar/static/src/scss/calendar.scss"/>
|
||||
<script type="text/javascript" src="/calendar/static/src/js/base_calendar.js"></script>
|
||||
<script type="text/javascript" src="/calendar/static/src/js/mail_activity.js"></script>
|
||||
<script type="text/javascript" src="/calendar/static/src/js/systray.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/calendar/static/tests/calendar_tests.js"/>
|
||||
<script type="text/javascript" src="/calendar/static/tests/systray_tests.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -321,6 +321,42 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="view_calendar_today_event_calendar" model="ir.ui.view">
|
||||
<field name="name">calendar.event.calendar</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="priority" eval="2"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Meetings" date_start="start" date_stop="stop" date_delay="duration" all_day="allday"
|
||||
readonly_form_view_id="%(calendar.view_calendar_event_form_popup)s"
|
||||
event_open_popup="true"
|
||||
event_limit="3"
|
||||
color="partner_id"
|
||||
mode="day">
|
||||
<field name="name"/>
|
||||
<field name="partner_ids" write_model="calendar.contacts" write_field="partner_id" avatar_field="image_small"/>
|
||||
<field name="is_highlighted" invisible="1"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record> -->
|
||||
|
||||
<!-- <record id="action_today_calendar_event" model="ir.actions.act_window">
|
||||
<field name="name">Today's Meetings</field>
|
||||
<field name="res_model">calendar.event</field>
|
||||
<field name="view_mode">calendar,tree,form</field>
|
||||
<field name="view_id" ref="view_calendar_today_event_calendar"/>
|
||||
<field name="search_view_id" ref="view_calendar_event_search"/>
|
||||
<field name="context">{'search_default_mymeetings': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Schedule a new meeting
|
||||
</p><p>
|
||||
The calendar is shared between employees and fully integrated with
|
||||
other applications such as the employee leaves or the business
|
||||
opportunities.
|
||||
</p>
|
||||
</field>
|
||||
</record> -->
|
||||
|
||||
<record id="action_view_calendar_event_calendar" model="ir.actions.act_window.view">
|
||||
<field name="act_window_id" ref="action_calendar_event"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
|
||||
@@ -98,6 +98,7 @@ class Users(models.Model):
|
||||
user_activities[activity['model']] = {
|
||||
'name': model_names[activity['id']],
|
||||
'model': activity['model'],
|
||||
'type': 'activity',
|
||||
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
|
||||
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
|
||||
}
|
||||
|
||||
@@ -180,9 +180,8 @@ var ActivityMenu = Widget.extend({
|
||||
return self._rpc({
|
||||
model: 'res.users',
|
||||
method: 'systray_get_activities',
|
||||
kwargs: {
|
||||
context: session.user_context,
|
||||
},
|
||||
args: [],
|
||||
kwargs: {context: session.user_context},
|
||||
}).then(function (data) {
|
||||
self.activities = data;
|
||||
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
|
||||
|
||||
@@ -67,10 +67,11 @@
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.o_mail_channel_preview {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
padding: 5px;
|
||||
.o_mail_channel_image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.o_channel_info {
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<t t-esc="activity.name"/>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div t-if="activity and activity.type == 'activity'">
|
||||
<button t-if="activity.overdue_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='overdue'><t t-esc="activity.overdue_count"/> Late </button>
|
||||
<span t-if="!activity.overdue_count" class="o_no_activity mr16">0 Late </span>
|
||||
<button t-if="activity.today_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='today'> <t t-esc="activity.today_count"/> Today </button>
|
||||
|
||||
@@ -20,6 +20,7 @@ QUnit.module('ActivityMenu', {
|
||||
fields: {
|
||||
name: { type: "char" },
|
||||
model: { type: "char" },
|
||||
type: { type: "char" },
|
||||
planned_count: { type: "integer"},
|
||||
today_count: { type: "integer"},
|
||||
overdue_count: { type: "integer"},
|
||||
@@ -28,6 +29,7 @@ QUnit.module('ActivityMenu', {
|
||||
records: [{
|
||||
name: "Contact",
|
||||
model: "res.partner",
|
||||
type: "activity",
|
||||
planned_count: 0,
|
||||
today_count: 1,
|
||||
overdue_count: 0,
|
||||
@@ -35,6 +37,7 @@ QUnit.module('ActivityMenu', {
|
||||
},
|
||||
{
|
||||
name: "Task",
|
||||
type: "activity",
|
||||
model: "project.task",
|
||||
planned_count: 1,
|
||||
today_count: 0,
|
||||
@@ -43,6 +46,7 @@ QUnit.module('ActivityMenu', {
|
||||
},
|
||||
{
|
||||
name: "Issue",
|
||||
type: "activity",
|
||||
model: "project.issue",
|
||||
planned_count: 1,
|
||||
today_count: 1,
|
||||
|
||||
@@ -63,6 +63,7 @@ GROUP BY id"""
|
||||
activities[note_index]['name'] = note_label
|
||||
else:
|
||||
activities.append({
|
||||
'type': 'activity',
|
||||
'name': note_label,
|
||||
'model': 'note.note',
|
||||
'icon': modules.module.get_module_icon(self.env['note.note']._original_module),
|
||||
|
||||
@@ -152,7 +152,7 @@ var CalendarView = AbstractView.extend({
|
||||
this.loadParams.fieldColor = attrs.color;
|
||||
|
||||
this.loadParams.filters = filters;
|
||||
this.loadParams.mode = attrs.mode;
|
||||
this.loadParams.mode = (params.context && params.context.default_mode) || attrs.mode;
|
||||
this.loadParams.initialDate = moment(params.initialDate || new Date());
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user