diff --git a/addons/calendar/models/calendar_contact.py b/addons/calendar/models/calendar_contact.py
index 6885450fa27..9a8a002872a 100644
--- a/addons/calendar/models/calendar_contact.py
+++ b/addons/calendar/models/calendar_contact.py
@@ -11,6 +11,8 @@ class Contacts(models.Model):
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user)
partner_id = fields.Many2one('res.partner', 'Employee', required=True)
active = fields.Boolean('Active', default=True)
+ partner_checked = fields.Boolean('Checked', default=True,
+ help="This field is used to know if the partner is checked in the filter of the calendar view for the user_id.")
_sql_constraints = [
('user_id_partner_id_unique', 'UNIQUE(user_id, partner_id)', 'A user cannot have the same contact twice.')
diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py
index a13afdd3a7d..fc3f8cbfa67 100644
--- a/addons/calendar/models/calendar_event.py
+++ b/addons/calendar/models/calendar_event.py
@@ -97,9 +97,9 @@ class Meeting(models.Model):
# description
name = fields.Char('Meeting Subject', required=True)
description = fields.Text('Description')
- user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user)
+ user_id = fields.Many2one('res.users', 'Organizer', default=lambda self: self.env.user)
partner_id = fields.Many2one(
- 'res.partner', string='Responsible Contact', related='user_id.partner_id', readonly=True)
+ 'res.partner', string='Scheduled by', related='user_id.partner_id', readonly=True)
location = fields.Char('Location', tracking=True, help="Location of Event")
videocall_location = fields.Char('Join Video Call', default=_default_videocall_location)
# visibility
@@ -119,6 +119,9 @@ class Meeting(models.Model):
that period of time.")
is_highlighted = fields.Boolean(
compute='_compute_is_highlighted', string='Is the Event Highlighted')
+ is_organizer_alone = fields.Boolean(compute='_compute_is_organizer_alone', string="Is the Organizer Alone",
+ help="""Check if the organizer is alone in the event, i.e. if the organizer is the only one that hasn't declined
+ the event (only if the organizer is not the only attendee)""")
# filtering
active = fields.Boolean(
'Active', default=True,
@@ -222,6 +225,20 @@ class Meeting(models.Model):
for event in self:
event.is_highlighted = False
+ @api.depends('partner_id', 'attendee_ids')
+ def _compute_is_organizer_alone(self):
+ """
+ Check if the organizer of the event is the only one who has accepted the event.
+ It does not apply if the organizer is the only attendee of the event because it
+ would represent a personnal event.
+ The goal of this field is to highlight to the user that the others attendees are
+ not available for this event.
+ """
+ for event in self:
+ organizer = event.attendee_ids.filtered(lambda a: a.partner_id == event.partner_id)
+ all_declined = not any((event.attendee_ids - organizer).filtered(lambda a: a.state != 'declined'))
+ event.is_organizer_alone = len(event.attendee_ids) > 1 and all_declined
+
def _compute_display_time(self):
for meeting in self:
meeting.display_time = self._get_display_time(meeting.start, meeting.stop, meeting.duration, meeting.allday)
@@ -287,11 +304,7 @@ class Meeting(models.Model):
def _compute_attendee(self):
for meeting in self:
- attendee = next(
- (attendee for attendee in self.attendee_ids
- if attendee.partner_id == self.env.user.partner_id),
- self.env['calendar.attendee']
- )
+ attendee = meeting._find_attendee()
meeting.attendee_status = attendee.state if attendee else 'needsAction'
@api.constrains('start', 'stop', 'start_date', 'stop_date')
@@ -364,8 +377,10 @@ class Meeting(models.Model):
activity_vals['user_id'] = user_id
values['activity_ids'] = [(0, 0, activity_vals)]
+ # Automatically add the current partner when creating an event if there is none (happens when we quickcreate an event)
+ self_partner_id = [(4, self.env.user.partner_id.id)]
vals_list = [
- dict(vals, attendee_ids=self._attendees_values(vals['partner_ids'])) if 'partner_ids' in vals else vals
+ dict(vals, attendee_ids=self._attendees_values(vals.get('partner_ids', self_partner_id)))
for vals in vals_list
]
recurrence_fields = self._get_recurrent_fields()
@@ -747,6 +762,28 @@ class Meeting(models.Model):
# TOOLS
# ------------------------------------------------------------
+ def _find_attendee(self):
+ """ Return the first attendee where the user connected has been invited
+ or the attendee selected in the filter that is the owner
+ from all the meeting_ids in parameters.
+ """
+ self.ensure_one()
+
+ my_attendee = self.attendee_ids.filtered(lambda att: att.partner_id == self.env.user.partner_id)
+ if my_attendee:
+ return my_attendee
+
+ event_checked_attendees = self.env['calendar.contacts'].search([
+ ('user_id', '=', self.env.user.id),
+ ('partner_id', 'in', self.attendee_ids.partner_id.ids),
+ ('partner_checked', '=', True)
+ ]).mapped('partner_id')
+ if self.partner_id in event_checked_attendees and self.partner_id in self.attendee_ids.partner_id:
+ return self.attendee_ids.filtered(lambda attendee: attendee.partner_id == self.partner_id)
+
+ attendee = self.attendee_ids.filtered(lambda att: att.partner_id in event_checked_attendees and att.state != "needsAction")
+ return attendee
+
def _get_start_date(self):
"""Return the event starting date in the event's timezone.
If no starting time is assigned (yet), return today as default
diff --git a/addons/calendar/models/res_partner.py b/addons/calendar/models/res_partner.py
index ec4f5ea7555..2cfe4551337 100644
--- a/addons/calendar/models/res_partner.py
+++ b/addons/calendar/models/res_partner.py
@@ -12,24 +12,29 @@ class Partner(models.Model):
calendar_last_notif_ack = fields.Datetime(
'Last notification marked as read from base Calendar', default=fields.Datetime.now)
- def get_attendee_detail(self, meeting_id):
- """ Return a list of tuple (id, name, status)
- Used by base_calendar.js : Many2ManyAttendee
+ def get_attendee_detail(self, meeting_ids):
+ """ Return a list of dict of the given meetings with the attendees details
+ Used by:
+ - base_calendar.js : Many2ManyAttendee
+ - calendar_model.js (calendar.CalendarModel)
"""
- datas = []
- meeting = None
- if meeting_id:
- meeting = self.env['calendar.event'].browse(meeting_id)
-
+ attendees_details = []
+ meetings = self.env['calendar.event'].browse(meeting_ids)
+ meetings_attendees = meetings.mapped('attendee_ids')
for partner in self:
- data = partner.name_get()[0]
- data = [data[0], data[1], False, partner.color]
- if meeting:
- for attendee in meeting.attendee_ids:
- if attendee.partner_id.id == partner.id:
- data[2] = attendee.state
- datas.append(data)
- return datas
+ partner_info = partner.name_get()[0]
+ for attendee in meetings_attendees.filtered(lambda att: att.partner_id == partner):
+ attendee_is_organizer = self.env.user == attendee.event_id.user_id and attendee.partner_id == self.env.user.partner_id
+ attendees_details.append({
+ 'id': partner_info[0],
+ 'name': partner_info[1],
+ 'color': partner.color,
+ 'status': attendee.state,
+ 'event_id': attendee.event_id.id,
+ 'attendee_id': attendee.id,
+ 'is_alone': attendee.event_id.is_organizer_alone and attendee_is_organizer,
+ })
+ return attendees_details
@api.model
def _set_calendar_last_notif_ack(self):
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
index 46362327e17..a4de081c604 100644
--- a/addons/calendar/static/src/js/base_calendar.js
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -127,12 +127,10 @@ BasicModel.include({
return this._rpc({
model: 'res.partner',
method: 'get_attendee_detail',
- args: [attendeeIDs, meetingID],
+ args: [attendeeIDs, [meetingID]],
context: context,
}).then(function (result) {
- return _.map(result, function (d) {
- return _.object(['id', 'display_name', 'status', 'color'], d);
- });
+ return result;
});
},
});
diff --git a/addons/calendar/static/src/js/calendar_controller.js b/addons/calendar/static/src/js/calendar_controller.js
index c87f756702c..afb48c1fbaf 100644
--- a/addons/calendar/static/src/js/calendar_controller.js
+++ b/addons/calendar/static/src/js/calendar_controller.js
@@ -40,6 +40,22 @@ odoo.define('calendar.CalendarController', function (require) {
});
},
+ /**
+ * If the event comes from the organizer we delete the event, else we the event is decline for the attendee
+ * @override
+ */
+ _onDeleteRecord: function (ev) {
+ const event = _.find(this.model.data.data, e => e.id === ev.data.id && e.attendee_id === ev.data.event.attendee_id);
+ if (this.getSession().partner_id === event.attendee_id && this.getSession().partner_id === event.record.partner_id[0]) {
+ this._super(...arguments);
+ } else {
+ var self = this;
+ this.model.declineEvent(event).then(function () {
+ self.reload();
+ });
+ }
+ },
+
/**
* @override
* @private
diff --git a/addons/calendar/static/src/js/calendar_model.js b/addons/calendar/static/src/js/calendar_model.js
index 8d90b543d0b..81d529e9bb9 100644
--- a/addons/calendar/static/src/js/calendar_model.js
+++ b/addons/calendar/static/src/js/calendar_model.js
@@ -14,7 +14,68 @@ odoo.define('calendar.CalendarModel', function (require) {
return _.extend({}, data, {
'recurrence_update': event.recurrenceUpdate,
});
- }
+ },
+ async _getCalendarEventData(events) {
+ const calendarEventData = await this._super(...arguments);
+ const calendarEventByAttendeeData = await this._calendarEventByAttendee(calendarEventData);
+ return calendarEventByAttendeeData;
+ },
+ /**
+ * Split the events to display an event for each attendee with the correct status.
+ * If the all filter is activated, we don't display an event for each attendee and keep
+ * the previous behavior to display a single event.
+ *
+ */
+ async _calendarEventByAttendee(eventsData) {
+ const self = this;
+ let eventsDataByAttendee = [];
+ const attendeeFilters = self.loadParams.filters.partner_ids;
+ const everyoneFilter = attendeeFilters && (attendeeFilters.filters.find(f => f.value === "all") || {}).active || false;
+ const attendeeIDs = attendeeFilters && _.filter(attendeeFilters.filters.map(partner => partner.value !== 'all' ? partner.value : false), id => id !== false);
+ const eventIDs = eventsData.map(event => event.id);
+ // Fetch the attendees' info from the partners selected in the filter to display the events
+ this.attendees = await self._rpc({
+ model: 'res.partner',
+ method: 'get_attendee_detail',
+ args: [attendeeIDs, eventIDs],
+ });
+ if (!everyoneFilter) {
+ eventsData.forEach(event => {
+ (event.record.partner_ids || []).forEach(attendee => {
+ if (attendeeFilters.filters.find(f => f.active && f.value == attendee)) {
+ let e = $.extend(true, {}, event);
+ e.attendee_id = attendee;
+ const attendee_info = self.attendees.find(a => a.id == attendee && a.event_id == e.record.id);
+ if (attendee_info) {
+ e.record.attendee_status = attendee_info.status;
+ e.record.is_alone = attendee_info.is_alone;
+ }
+ eventsDataByAttendee.push(e);
+ }
+ });
+ });
+ } else {
+ eventsData.forEach(event => {
+ const attendee_info = self.attendees.find(a => a.id == self.getSession().partner_id && a.event_id == event.record.id);
+ if (attendee_info) {
+ event.record.is_alone = attendee_info.is_alone;
+ }
+ });
+ }
+ return eventsDataByAttendee.length ? eventsDataByAttendee : eventsData;
+ },
+
+ /**
+ * Decline an event for the actual attendee
+ * @param {Integer} eventId
+ */
+ declineEvent: function (event) {
+ return this._rpc({
+ model: 'calendar.attendee',
+ method: 'do_decline',
+ args: [this.attendees.find(attendee => attendee.event_id === event.id && attendee.id === this.getSession().partner_id).attendee_id],
+ });
+ },
});
return CalendarModel;
diff --git a/addons/calendar/static/src/js/calendar_renderer.js b/addons/calendar/static/src/js/calendar_renderer.js
index 8320dc84f75..3ccd924ea7b 100644
--- a/addons/calendar/static/src/js/calendar_renderer.js
+++ b/addons/calendar/static/src/js/calendar_renderer.js
@@ -5,7 +5,6 @@ const CalendarRenderer = require('web.CalendarRenderer');
const CalendarPopover = require('web.CalendarPopover');
const session = require('web.session');
-
const AttendeeCalendarPopover = CalendarPopover.extend({
template: 'Calendar.attendee.status.popover',
events: _.extend({}, CalendarPopover.prototype.events, {
@@ -32,18 +31,24 @@ const AttendeeCalendarPopover = CalendarPopover.extend({
// Public
//--------------------------------------------------------------------------
+ /**
+ * @return {boolean}
+ */
+ isCurrentPartnerOrganizer() {
+ return this.event.extendedProps.record.partner_id[0] === session.partner_id;
+ },
/**
* @return {boolean}
*/
isCurrentPartnerAttendee() {
- return this.event.extendedProps.record.partner_ids.includes(session.partner_id);
+ return this.event.extendedProps.record.partner_ids.includes(session.partner_id) && this.event.extendedProps.attendee_id === session.partner_id;
},
/**
* @override
* @return {boolean}
*/
isEventDeletable() {
- return this._super() && (this._isEventPrivate() ? this.isCurrentPartnerAttendee() : true);
+ return this._super() && this.isCurrentPartnerAttendee();
},
/**
* @override
@@ -92,6 +97,8 @@ const AttendeeCalendarPopover = CalendarPopover.extend({
self.event.extendedProps.record.attendee_status = selectedStatus; // FIXEME: Maybe we have to reload view
self.$('.o-calendar-attendee-status-text').text(self.statusInfo[selectedStatus].text);
self.$('.o-calendar-attendee-status-icon').removeClass(_.values(self.statusColors).join(' ')).addClass(self.statusInfo[selectedStatus].color);
+ self.$el.parent().hide();
+ self.trigger_up('render_event');
});
},
});
@@ -99,10 +106,35 @@ const AttendeeCalendarPopover = CalendarPopover.extend({
const AttendeeCalendarRenderer = CalendarRenderer.extend({
config: _.extend({}, CalendarRenderer.prototype.config, {
- CalendarPopover: AttendeeCalendarPopover,
- }),
+ CalendarPopover: AttendeeCalendarPopover,
+ eventTemplate: 'Calendar.calendar-box',
+ }),
+ /**
+ * Add the attendee-id attribute in order to distinct the events when there are
+ * several attendees in the event.
+ * @override
+ */
+ _addEventAttributes: function (element, event) {
+ this._super(...arguments);
+ element.attr('data-attendee-id', event.extendedProps.attendee_id);
+ },
+ /**
+ * If an attendee_id has been set on the event, we check also the attendee-id attribute
+ * to select the good event on which the CSS class will be applied.
+ * @override
+ */
+ _computeEventSelector: function (info) {
+ let selector = this._super(...arguments);
+ if (info.event.extendedProps.attendee_id) {
+ selector += `[data-attendee-id=${info.event.extendedProps.attendee_id}]`;
+ }
+ return selector;
+ },
});
-return AttendeeCalendarRenderer
+return {
+ AttendeeCalendarRenderer: AttendeeCalendarRenderer,
+ AttendeeCalendarPopover: AttendeeCalendarPopover,
+};
});
diff --git a/addons/calendar/static/src/js/calendar_view.js b/addons/calendar/static/src/js/calendar_view.js
index 4454dc0e1ef..78eb7ec2752 100644
--- a/addons/calendar/static/src/js/calendar_view.js
+++ b/addons/calendar/static/src/js/calendar_view.js
@@ -3,7 +3,7 @@ odoo.define('calendar.CalendarView', function (require) {
var CalendarController = require('calendar.CalendarController');
var CalendarModel = require('calendar.CalendarModel');
-const CalendarRenderer = require('calendar.CalendarRenderer');
+const CalendarRenderer = require('calendar.CalendarRenderer').AttendeeCalendarRenderer;
var CalendarView = require('web.CalendarView');
var viewRegistry = require('web.view_registry');
diff --git a/addons/calendar/static/src/xml/base_calendar.xml b/addons/calendar/static/src/xml/base_calendar.xml
index 3f2b083477e..b87fa243da4 100644
--- a/addons/calendar/static/src/xml/base_calendar.xml
+++ b/addons/calendar/static/src/xml/base_calendar.xml
@@ -62,4 +62,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/calendar/static/tests/calendar_tests.js b/addons/calendar/static/tests/calendar_tests.js
index d2626b0ab51..004e9b7ad65 100644
--- a/addons/calendar/static/tests/calendar_tests.js
+++ b/addons/calendar/static/tests/calendar_tests.js
@@ -51,11 +51,11 @@ QUnit.module('calendar', {
"the method should only be called on res.partner");
assert.deepEqual(args.args[0], [1, 2],
"the partner ids should be passed as argument");
- assert.strictEqual(args.args[1], 14,
+ assert.deepEqual(args.args[1], [14],
"the event id should be passed as argument");
return Promise.resolve([
- [1, "Jesus", "accepted", 0],
- [2, "Mahomet", "tentative", 0],
+ {id: 1, name: "Jesus", status: "accepted", color: 0},
+ {id: 2, name: "Mahomet", status: "tentative", color: 0},
]);
}
return this._super.apply(this, arguments);
diff --git a/addons/calendar/tests/test_attendees.py b/addons/calendar/tests/test_attendees.py
index 44f7177c149..d9e1c1db4fc 100644
--- a/addons/calendar/tests/test_attendees.py
+++ b/addons/calendar/tests/test_attendees.py
@@ -11,12 +11,12 @@ class TestEventNotifications(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.event = cls.env['calendar.event'].create({
+ cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
+ cls.event = cls.env['calendar.event'].with_user(cls.user).create({
'name': "Doom's day",
'start': datetime(2019, 10, 25, 8, 0),
'stop': datetime(2019, 10, 27, 18, 0),
}).with_context(mail_notrack=True)
- cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
cls.partner = cls.user.partner_id
def test_attendee_added(self):
diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml
index 0ec4a0bac11..684495142c5 100644
--- a/addons/calendar/views/calendar_views.xml
+++ b/addons/calendar/views/calendar_views.xml
@@ -163,8 +163,9 @@
+
+
-
@@ -253,12 +254,6 @@
-
-
-
-
-
-
@@ -277,13 +272,14 @@
+ color="partner_ids">
-
-
+
+
+
diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py
index c10a47c794d..d506ca28ba0 100644
--- a/addons/google_calendar/models/calendar.py
+++ b/addons/google_calendar/models/calendar.py
@@ -114,7 +114,7 @@ class Meeting(models.Model):
user = google_event.owner(self.env)
google_attendees += [{
'email': user.partner_id.email,
- 'status': {'response': 'accepted'},
+ 'responseStatus': 'accepted',
}]
emails = [a.get('email') for a in google_attendees]
existing_attendees = self.env['calendar.attendee']
@@ -195,7 +195,6 @@ class Meeting(models.Model):
'method': "email" if alarm.alarm_type == "email" else "popup",
'minutes': alarm.duration_minutes
} for alarm in self.alarm_ids]
- attendee_ids = self.attendee_ids.filtered(lambda a: a.partner_id not in self.user_id.partner_id)
values = {
'id': self.google_id,
'start': start,
diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js
index d168faeb154..3712dbe6e86 100644
--- a/addons/google_calendar/static/src/js/google_calendar.js
+++ b/addons/google_calendar/static/src/js/google_calendar.js
@@ -5,7 +5,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
const CalendarView = require('calendar.CalendarView');
-const CalendarRenderer = require('calendar.CalendarRenderer');
+const CalendarRenderer = require('calendar.CalendarRenderer').AttendeeCalendarRenderer;
const CalendarController = require('calendar.CalendarController');
const CalendarModel = require('calendar.CalendarModel');
const viewRegistry = require('web.view_registry');
diff --git a/addons/google_calendar/static/src/js/google_calendar_popover.js b/addons/google_calendar/static/src/js/google_calendar_popover.js
index aae4a1897eb..497cc09d086 100644
--- a/addons/google_calendar/static/src/js/google_calendar_popover.js
+++ b/addons/google_calendar/static/src/js/google_calendar_popover.js
@@ -1,7 +1,7 @@
odoo.define('google_calendar.GoogleCalendarPopover', function(require) {
"use strict";
- const CalendarPopover = require('web.CalendarPopover');
+ const CalendarPopover = require('calendar.CalendarRenderer').AttendeeCalendarPopover;
const GoogleCalendarPopover = CalendarPopover.include({
events: _.extend({}, CalendarPopover.prototype.events, {
@@ -9,7 +9,7 @@ odoo.define('google_calendar.GoogleCalendarPopover', function(require) {
}),
isGEventSyncedAndArchivable() {
- return this.event.extendedProps.record.google_id;
+ return this.isCurrentPartnerOrganizer() && this.event.extendedProps.record.google_id;
},
isEventDeletable() {
diff --git a/addons/google_calendar/static/tests/google_calendar_tests.js b/addons/google_calendar/static/tests/google_calendar_tests.js
index db98e22929f..7129fda6a42 100644
--- a/addons/google_calendar/static/tests/google_calendar_tests.js
+++ b/addons/google_calendar/static/tests/google_calendar_tests.js
@@ -87,7 +87,7 @@ QUnit.module('Google Calendar', {
viewOptions: {
initialDate: initialDate,
},
- mockRPC: function (route, args) {
+ mockRPC: async function (route, args) {
if (route === '/google_calendar/sync_data') {
assert.step(route);
this.data['calendar.event'].records.push(
@@ -98,8 +98,10 @@ QUnit.module('Google Calendar', {
assert.step(route);
} else if (route === '/microsoft_calendar/sync_data') {
return Promise.resolve();
+ } else if (route === '/web/dataset/call_kw/res.partner/get_attendee_detail') {
+ return Promise.resolve([]);
}
- return this._super.apply(this, arguments);
+ return await this._super.apply(this, arguments);
},
});
diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py
index 43db260ebd0..7feafb24fec 100644
--- a/addons/google_calendar/tests/test_sync_odoo2google.py
+++ b/addons/google_calendar/tests/test_sync_odoo2google.py
@@ -89,7 +89,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'reminders': {'overrides': [], 'useDefault': False},
'organizer': {'email': 'odoobot@example.com', 'self': True},
- 'attendees': [],
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
})
@@ -148,7 +148,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'reminders': {'overrides': [], 'useDefault': False},
'organizer': {'email': 'odoobot@example.com', 'self': True},
- 'attendees': [],
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}}
})
@@ -183,7 +183,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'reminders': {'overrides': [], 'useDefault': False},
'organizer': {'email': 'odoobot@example.com', 'self': True},
- 'attendees': [],
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.recurrence_id.id}}
}, timeout=3)
@@ -229,7 +229,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'location': '',
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
- 'attendees': [],
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}},
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
@@ -315,7 +315,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'location': '',
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
- 'attendees': [],
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}},
'reminders': {'overrides': [], 'useDefault': False},
diff --git a/addons/hr_holidays/tests/test_company_leave.py b/addons/hr_holidays/tests/test_company_leave.py
index 31d3b9cf38b..53b528a5cc8 100644
--- a/addons/hr_holidays/tests/test_company_leave.py
+++ b/addons/hr_holidays/tests/test_company_leave.py
@@ -313,7 +313,7 @@ class TestCompanyLeave(TransactionCase):
})
company_leave._compute_date_from_to()
- with self.assertQueryCount(__system__=739, admin=865):
+ with self.assertQueryCount(__system__=842, admin=865):
# Original query count: 1987
# Without tracking/activity context keys: 5154
company_leave.action_validate()
diff --git a/addons/hr_work_entry_holidays/tests/test_performance.py b/addons/hr_work_entry_holidays/tests/test_performance.py
index cd823539586..e9dc9d2a23c 100644
--- a/addons/hr_work_entry_holidays/tests/test_performance.py
+++ b/addons/hr_work_entry_holidays/tests/test_performance.py
@@ -31,7 +31,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
- with self.assertQueryCount(__system__=92, admin=99):
+ with self.assertQueryCount(__system__=94, admin=99):
leave.action_validate()
leave.action_refuse()
diff --git a/addons/microsoft_calendar/models/calendar_attendee.py b/addons/microsoft_calendar/models/calendar_attendee.py
index 062bdb813b0..2a10f7f2a6c 100644
--- a/addons/microsoft_calendar/models/calendar_attendee.py
+++ b/addons/microsoft_calendar/models/calendar_attendee.py
@@ -4,12 +4,24 @@
from odoo import models
from odoo.addons.microsoft_calendar.models.microsoft_sync import microsoft_calendar_token
+from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
class Attendee(models.Model):
_name = 'calendar.attendee'
_inherit = 'calendar.attendee'
+ def write(self, vals):
+ """
+ Tell to resync the link microsoft/odoo of the event when the status of an attendee is modified
+ """
+ res = super().write(vals)
+ if vals.get('state'):
+ # When the state is changed, the corresponding event must be sync with microsoft
+ microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
+ self.event_id.filtered('microsoft_id')._sync_odoo2microsoft(microsoft_service)
+ return res
+
def _send_mail_to_attendees(self, mail_template, force_send=False):
""" Override the super method
If not synced with Microsoft Outlook, let Odoo in charge of sending emails
diff --git a/addons/microsoft_calendar/models/microsoft_sync.py b/addons/microsoft_calendar/models/microsoft_sync.py
index 5145002d992..ac4424f7c20 100644
--- a/addons/microsoft_calendar/models/microsoft_sync.py
+++ b/addons/microsoft_calendar/models/microsoft_sync.py
@@ -56,6 +56,7 @@ class MicrosoftSync(models.AbstractModel):
_description = "Synchronize a record with Microsoft Calendar"
microsoft_id = fields.Char('Microsoft Calendar Id', copy=False)
+ # This field helps to known when a microsoft event need to be resynced
need_sync_m = fields.Boolean(default=True, copy=False)
active = fields.Boolean(default=True)
diff --git a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
index 8753814363c..28acaafe6be 100644
--- a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
+++ b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
@@ -5,7 +5,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
const CalendarView = require('calendar.CalendarView');
-const CalendarRenderer = require('calendar.CalendarRenderer');
+const CalendarRenderer = require('calendar.CalendarRenderer').AttendeeCalendarRenderer;
const CalendarController = require('calendar.CalendarController');
const CalendarModel = require('calendar.CalendarModel');
const viewRegistry = require('web.view_registry');
diff --git a/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js b/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js
index 76c64b5f224..e80e525d765 100644
--- a/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js
+++ b/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js
@@ -1,7 +1,7 @@
odoo.define('microsoft_calendar.MicrosoftCalendarPopover', function(require) {
"use strict";
- const CalendarPopover = require('web.CalendarPopover');
+ const CalendarPopover = require('calendar.CalendarRenderer').AttendeeCalendarPopover;
const MicrosoftCalendarPopover = CalendarPopover.include({
events: _.extend({}, CalendarPopover.prototype.events, {
@@ -15,9 +15,9 @@ odoo.define('microsoft_calendar.MicrosoftCalendarPopover', function(require) {
*/
isMEventSyncedAndArchivable() {
if (this.event.extendedProps.record.google_id === undefined) {
- return this.event.extendedProps.record.microsoft_id;
+ return this.isCurrentPartnerOrganizer() && this.event.extendedProps.record.microsoft_id;
}
- return !this.event.extendedProps.record.google_id && this.event.extendedProps.record.microsoft_id
+ return this.isCurrentPartnerOrganizer() && !this.event.extendedProps.record.google_id && this.event.extendedProps.record.microsoft_id
},
isEventDeletable() {
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 6ec90393b7a..b54e338a808 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -246,7 +246,23 @@ var CalendarController = AbstractController.extend({
* @private
* @param {OdooEvent} event
*/
- _onChangeFilter: function (event) {
+ _onChangeFilter: async function (event) {
+ if (event.data.value !== "all" && event.target.filter_field) {
+ const domain = [['user_id', '=', this.getSession().uid], [event.target.write_field, '=', parseInt(event.data.value)]];
+ const contact = await this._rpc({
+ model: event.target.write_model,
+ method: 'search',
+ args: [domain],
+ });
+ let val = {
+ [event.target.filter_field]: event.data.active,
+ };
+ await this._rpc({
+ model: event.target.write_model,
+ method: 'write',
+ args: [contact, val],
+ });
+ }
if (this.model.changeFilter(event.data) && !event.data.no_reload) {
this.reload();
}
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index cb0eeec5143..34e504d2fc3 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -499,9 +499,9 @@ return AbstractModel.extend({
fields: self.fieldNames,
domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
})
- .then(function (events) {
+ .then(async function (events) {
self._parseServerData(events);
- self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
+ self.data.data = await self._getCalendarEventData(events);
return Promise.all([
self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data)
@@ -518,14 +518,44 @@ return AbstractModel.extend({
_loadColors: function (element, events) {
if (this.fieldColor) {
var fieldName = this.fieldColor;
+ var self = this;
_.each(events, function (event) {
var value = event.record[fieldName];
- event.color_index = _.isArray(value) ? value[0] : value;
+ var filter = self.loadParams.filters[fieldName];
+ var color = filter && _.map(filter.filters, f => f.value) || value;
+ var everyoneFilter = filter && (_.find(filter.filters, f => f.value === "all") || {}).active || false;
+ if (!everyoneFilter) {
+ color = [event.attendee_id || value]
+ } else {
+ color = value.includes(self.getSession().partner_id) ? self.getSession().partner_id : value
+ }
+ event.color_index = _.isArray(color) ? self._getColorIndex(value, color[0]) : color;
});
this.model_color = this.fields[fieldName].relation || element.model;
}
return Promise.resolve();
},
+ /**
+ * Get the color index used to render the event and the filter.
+ * Since we have a maximum of 30 colors, we have to adapt the color
+ * index in case the id exceeds the number of colors available.
+ *
+ * - actual_color_list contains the list of color already used in the filter
+ * - color_index is the color we try to apply for the event
+ *
+ * @private
+ * @param {Array} actual_color_list
+ * @param {Integer} color_index
+ * @returns {Integer}
+ */
+ _getColorIndex: function(actual_color_list, color_index) {
+ var actual_color_list = actual_color_list || [0];
+ var new_color_index = color_index % 30;
+ if (new_color_index !== color_index && actual_color_list.includes(new_color_index)) {
+ new_color_index = this._getColorIndex(actual_color_list, new_color_index + 3);
+ }
+ return new_color_index;
+ },
/**
* @private
* @param {any} filter
@@ -537,11 +567,15 @@ return AbstractModel.extend({
}
var field = this.fields[filter.fieldName];
+ var fields = [filter.write_field];
+ if (filter.filter_field) {
+ fields.push(filter.filter_field);
+ }
return this._rpc({
model: filter.write_model,
method: 'search_read',
domain: [["user_id", "=", session.uid]],
- fields: [filter.write_field],
+ fields: fields,
})
.then(function (res) {
var records = _.map(res, function (record) {
@@ -554,7 +588,7 @@ return AbstractModel.extend({
'id': record.id,
'value': value,
'label': formater(_value, field),
- 'active': f && f.active,
+ 'active': (f && f.active) || (filter.filter_field && record[filter.filter_field]),
};
});
records.sort(function (f1,f2) {
@@ -609,7 +643,7 @@ return AbstractModel.extend({
if (filter.write_model) {
if (field.relation === self.model_color) {
_.each(filter.filters, function (f) {
- f.color_index = f.value;
+ f.color_index = self._getColorIndex(_.map(filter.filters, f => f.value), f.value);
});
}
return;
@@ -773,6 +807,9 @@ return AbstractModel.extend({
return r;
},
+ _getCalendarEventData: function (events) {
+ return _.map(events, this._recordToCalendarEvent.bind(this));
+ }
});
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index fed9a705d11..8cc5b39a0ca 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -46,6 +46,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
this.fieldName = options.fieldName;
this.write_model = options.write_model;
this.write_field = options.write_field;
+ this.filter_field = options.filter_field;
this.avatar_field = options.avatar_field;
this.avatar_model = options.avatar_model;
this.filters = options.filters;
@@ -161,10 +162,12 @@ return AbstractRenderer.extend({
template: "CalendarView",
config: {
CalendarPopover: CalendarPopover,
+ eventTemplate: 'calendar-box',
},
custom_events: _.extend({}, AbstractRenderer.prototype.custom_events || {}, {
edit_event: '_onEditEvent',
delete_event: '_onDeleteEvent',
+ render_event: '_renderEvents',
}),
/**
@@ -356,7 +359,7 @@ return AbstractRenderer.extend({
if (_.isEmpty(qweb_context.record)) {
return '';
} else {
- return qweb.render("calendar-box", qweb_context);
+ return qweb.render(this.config.eventTemplate, qweb_context);
}
},
/**
@@ -412,7 +415,7 @@ return AbstractRenderer.extend({
eventClickInfo.jsEvent.stopPropagation();
var eventData = eventClickInfo.event;
self._unselectEvent();
- $(self.calendarElement).find(`[data-event-id=${eventData.id}]`).addClass('o_cw_custom_highlight');
+ $(self.calendarElement).find(self._computeEventSelector(eventClickInfo)).addClass('o_cw_custom_highlight');
self._renderEventPopover(eventData, $(eventClickInfo.el));
},
selectAllow: function (event) {
@@ -446,7 +449,7 @@ return AbstractRenderer.extend({
var event = info.event;
var element = $(info.el);
var view = info.view;
- element.attr('data-event-id', event.id);
+ self._addEventAttributes(element, event);
if (view.type === 'dayGridYear') {
const color = this.getColor(event.extendedProps.color_index);
if (typeof color === 'string') {
@@ -471,6 +474,7 @@ return AbstractRenderer.extend({
if (view.type === 'dayGridMonth' && event.extendedProps.record) {
var start = event.extendedProps.r_start || event.start;
var end = event.extendedProps.r_end || event.end;
+ $(this.el).find(_.str.sprintf('.fc-day[data-date="%s"]', moment(start).format('YYYY-MM-DD'))).addClass('fc-has-event');
// Detect if the event occurs in just one day
// note: add & remove 1 min to avoid issues with 00:00
var isSameDayEvent = moment(start).clone().add(1, 'minute').isSame(moment(end).clone().subtract(1, 'minute'), 'day');
@@ -503,13 +507,13 @@ return AbstractRenderer.extend({
// The css ":hover" selector can't be used because these events
// are rendered using multiple elements.
eventMouseEnter: function (mouseEnterInfo) {
- $(self.calendarElement).find(`[data-event-id=${mouseEnterInfo.event.id}]`).addClass('o_cw_custom_hover');
+ $(self.calendarElement).find(self._computeEventSelector(mouseEnterInfo)).addClass('o_cw_custom_hover');
},
eventMouseLeave: function (mouseLeaveInfo) {
if (!mouseLeaveInfo.event.id) {
return;
}
- $(self.calendarElement).find(`[data-event-id=${mouseLeaveInfo.event.id}]`).removeClass('o_cw_custom_hover');
+ $(self.calendarElement).find(self._computeEventSelector(mouseLeaveInfo)).removeClass('o_cw_custom_hover');
},
eventDragStart: function (mouseDragInfo) {
$(self.calendarElement).find(`[data-event-id=${mouseDragInfo.event.id}]`).addClass('o_cw_custom_hover');
@@ -547,6 +551,22 @@ return AbstractRenderer.extend({
options.plugins.push(createYearCalendarView(FullCalendar, options));
return options;
},
+ /**
+ * Compute the selector to select the correct event we are currently on
+ * @param {jQueryElement} info
+ * @returns {String}
+ */
+ _computeEventSelector: function (info) {
+ return `[data-event-id=${info.event.id}]`;
+ },
+ /**
+ * Add attribute to the element
+ * @param {jQueryElement} element
+ * @param {Object} event
+ */
+ _addEventAttributes: function (element, event) {
+ element.attr('data-event-id', event.id);
+ },
/**
* Initialize the main calendar
*
@@ -880,13 +900,15 @@ return AbstractRenderer.extend({
for (const event of events) {
const start = moment(event.extendedProps.r_start);
const end = moment(event.extendedProps.r_end);
+ const duration = moment.duration(end.diff(start)).asDays();
+ event.extendedProps.record.start_hour = event.extendedProps.record.allday === false && duration < 1 ? start.format("HH:mm") : "";
const key = this._getFormattedDate(start, end, false, event.extendedProps.record.allday);
if (!(key in groupedEvents)) {
groupedEvents[key] = [];
groupKeys.push({
key: key,
- start: event.extendedProps.r_start,
- end: event.extendedProps.r_end,
+ start: event.start,
+ end: event.end,
isSameDayEvent: start.clone().add(1, 'minute')
.isSame(end.clone().subtract(1, 'minute'), 'day'),
});
@@ -1008,7 +1030,7 @@ return AbstractRenderer.extend({
*/
_onDeleteEvent: function (event) {
this._unselectEvent();
- this.trigger_up('deleteRecord', {id: parseInt(event.data.id, 10)});
+ this.trigger_up('deleteRecord', {id: parseInt(event.data.id, 10), event: event.target.event.extendedProps});
},
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 8894d0760e5..7ac7e1730c9 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -112,6 +112,7 @@ var CalendarView = AbstractView.extend({
};
filters[fieldName].write_model = child.attrs.write_model;
filters[fieldName].write_field = child.attrs.write_field; // can't use a x2many fields
+ filters[fieldName].filter_field = child.attrs.filter_field;
modelFilters.push(fields[fieldName].relation);
}
diff --git a/addons/web/static/src/scss/web_calendar.scss b/addons/web/static/src/scss/web_calendar.scss
index c847146a069..798c2adff0c 100644
--- a/addons/web/static/src/scss/web_calendar.scss
+++ b/addons/web/static/src/scss/web_calendar.scss
@@ -46,7 +46,7 @@ $o-cw-filter-avatar-size: 20px;
.fc-content {
white-space: normal;
- margin: 8px 4px;
+ margin: 0.25rem;
font-size: 1.1em;
font-weight: 500;
}
@@ -107,6 +107,20 @@ $o-cw-filter-avatar-size: 20px;
color: white;
}
+ .fc-more-popover {
+ .fc-header {
+ padding-left: 1rem;
+ .fc-title {
+ font-weight: bold;
+ }
+ }
+
+ .fc-body {
+ max-height: 500px;
+ overflow: auto;
+ }
+ }
+
.o_calendar_disabled {
background-color: $gray-200;
border-color: white !important;
@@ -215,6 +229,10 @@ $o-cw-filter-avatar-size: 20px;
padding-top: 0;
padding-bottom: 0;
}
+
+ .fc-content {
+ margin-top: 1px!important;
+ }
}
// ====== Week only
@@ -361,6 +379,37 @@ $o-cw-filter-avatar-size: 20px;
padding-left: 5px;
}
+ @for $i from 1 through length($o-colors-complete) {
+ $color: nth($o-colors-complete, $i);
+ .o_cw_popover_link.o_calendar_color_#{$i - 1} {
+ border: solid 2px;
+ padding-left: 5px;
+ border-color: $color;
+ margin-top: 2px;
+ background: $color;
+ color: black;
+
+ &.o_attendee_status_tentative {
+ color: color-yiq($color);
+ background: repeating-linear-gradient(
+ 45deg,
+ $color,
+ $color 10px,
+ rgba($color, 0.7) 10px,
+ rgba($color, 0.7) 20px) !important;
+ }
+
+ &.o_attendee_status_needsAction {
+ background-color: rgba(white, 0.9)!important;
+ }
+
+ &.o_attendee_status_declined {
+ text-decoration: line-through;
+ background-color: rgba(white, 0.9)!important;
+ }
+ }
+ }
+
> table {
height: 100%;
}
@@ -421,6 +470,10 @@ $o-cw-filter-avatar-size: 20px;
.fc-dayGridMonth-view {
padding-left: unset;
+
+ .fc-has-event {
+ background-color: #B4DFF5;
+ }
}
.fc-week.fc-row {
@@ -759,7 +812,13 @@ $o-cw-filter-avatar-size: 20px;
) !important;
.fc-bg {
- background: rgba(white, 0.5);
+ background: repeating-linear-gradient(
+ 45deg,
+ $color,
+ $color 10px,
+ rgba($color, 0.5) 10px,
+ rgba($color, 0.5) 20px
+ );
}
}
.fc-bgevent.o_calendar_color_#{$i - 1} {
@@ -770,7 +829,7 @@ $o-cw-filter-avatar-size: 20px;
.fc-event.o_calendar_color_#{$i - 1} {
border-color: $color;
color: darken($color, 45%);
- opacity: 0.75;
+ opacity: 1;
&.o_event_hightlight {
opacity: 1;
@@ -790,15 +849,48 @@ $o-cw-filter-avatar-size: 20px;
&.o_cw_custom_hover, &.o_cw_custom_highlight {
box-shadow: 0 12px 12px -5px rgba($color, 0.3);
color: color-yiq($color);
+ z-index: 10!important;
+ opacity: 1;
+ right: 1px!important;
+ left: 1px!important;
+ margin-right: 0!important;
+ }
+ &.o_attendee_status_needsAction, &.o_attendee_status_tentative, &.o_attendee_status_declined, &.o_attendee_status_alone {
+ border-width: 2px 2px 2px !important;
+
+ &.o_cw_custom_highlight {
+ background-color: $color;
+ }
+ }
+
+ &.o_attendee_status_tentative {
+ color: color-yiq($color);
.fc-bg {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
- rgba($color, 0.5) 10px,
- rgba($color, 0.5) 20px
- );
+ rgba(white, 0.4) 10px,
+ rgba(white, 0.4) 20px
+ ) !important;
+ }
+ }
+
+ &.o_attendee_status_alone, &.o_attendee_status_needsAction {
+ background-color: rgba(white, 0.9)!important;
+ color: black;
+ .fc-bg {
+ background-color: rgba(white, 0.9)!important;
+ }
+ }
+
+ &.o_attendee_status_declined {
+ text-decoration: line-through;
+ background-color: rgba(white, 0.9)!important;
+ color: black;
+ .fc-bg {
+ background-color: rgba(white, 0.9)!important;
}
}
}
diff --git a/addons/web/static/src/xml/web_calendar.xml b/addons/web/static/src/xml/web_calendar.xml
index 274c6f7e867..04dc186413e 100644
--- a/addons/web/static/src/xml/web_calendar.xml
+++ b/addons/web/static/src/xml/web_calendar.xml
@@ -132,10 +132,13 @@
-
+
+ t-att-data-title="record.display_name">
+
+
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 9f99431ae43..1560c4a15b9 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -116,11 +116,12 @@ QUnit.module('Views', {
id: {string: "ID", type: "integer"},
user_id: {string: "user", type: "many2one", relation: 'user'},
partner_id: {string: "partner", type: "many2one", relation: 'partner'},
+ partner_checked: {string: "checked", type: "boolean"}
},
records: [
- {id: 1, user_id: session.uid, partner_id: 1},
- {id: 2, user_id: session.uid, partner_id: 2},
- {id: 3, user_id: 4, partner_id: 3}
+ {id: 1, user_id: session.uid, partner_id: 1, partner_checked: true},
+ {id: 2, user_id: session.uid, partner_id: 2, partner_checked: true},
+ {id: 3, user_id: 4, partner_id: 3, partner_checked: true}
]
},
};
@@ -2140,6 +2141,40 @@ QUnit.module('Views', {
assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
});
+ QUnit.test('check filters with filter_field specified', async function (assert) {
+ assert.expect(5);
+
+ const calendar = await createCalendarView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: `
+
+
+ `,
+ });
+
+ assert.containsOnce(calendar, '.o_calendar_filter_item[data-id="2"] input:checked',
+ "checkbox should be checked");
+ await testUtils.dom.click(calendar.$('.o_calendar_filter_item[data-id="2"] input'));
+ assert.containsNone(calendar, '.o_calendar_filter_item[data-id="2"] input:checked',
+ "checkbox should not be checked");
+ assert.strictEqual(this.data.filter_partner.records.find(r => r.id === 2).partner_checked, false,
+ "the status of this filter should now be false");
+ await calendar.reload();
+ assert.containsNone(calendar, '.o_calendar_filter_item[data-id="2"] input:checked',
+ "checkbox should not be checked after the reload");
+ assert.strictEqual(this.data.filter_partner.records.find(r => r.id === 2).partner_checked, false,
+ "the status of this filter should still be false after the reload");
+ calendar.destroy();
+ });
+
QUnit.test('"all" filter', async function (assert) {
assert.expect(8);
@@ -3832,7 +3867,7 @@ QUnit.module('Views', {
assert.containsOnce(calendar, '.o_cw_popover');
popoverText = calendar.el.querySelector('.o_cw_popover')
.textContent.replace(/\s{2,}/g, ' ').trim();
- assert.strictEqual(popoverText, 'December 12, 2016 event 2 event 3');
+ assert.strictEqual(popoverText, 'December 12, 2016 event 2 10:55 event 3 15:55');
await testUtils.dom.click(calendar.el.querySelector('.o_cw_popover_close'));
assert.containsNone(calendar, '.o_cw_popover');
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 935c426bc72..4acbe4259af 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -454,10 +454,11 @@ calendar view are:
* ``avatar_field``
only for x2many field, to display the avatar instead of the display_name
in the cards
- * ``write_model`` and ``write_field``
+ * ``write_model`` and ``write_field`` and ``filter_field``
you can add a filter and save the result in the defined model, the
- filter is added in the sidebar
+ filter is added in the sidebar. The ``filter_field`` is optionnal and allows
+ you to specify the field that will hold the status of the filter.
* ``filter`` and ``color``
use "True" to add this field in filter in the sidebar. You can specify
diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng
index e2f2b05d39a..9c6d842d343 100644
--- a/odoo/addons/base/rng/common.rng
+++ b/odoo/addons/base/rng/common.rng
@@ -265,6 +265,7 @@
+