diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py
index 7d9fee82036..2242b7b9f10 100644
--- a/addons/calendar/models/calendar_event.py
+++ b/addons/calendar/models/calendar_event.py
@@ -176,6 +176,7 @@ class Meeting(models.Model):
res_model_id = fields.Many2one('ir.model', 'Document Model', ondelete='cascade')
res_model = fields.Char(
'Document Model Name', related='res_model_id.model', readonly=True, store=True)
+ res_model_name = fields.Char(related='res_model_id.name')
# messaging
activity_ids = fields.One2many('mail.activity', 'calendar_event_id', string='Activities')
# attendees
diff --git a/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.js b/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.js
index 12f53dab45f..04857c04169 100644
--- a/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.js
+++ b/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.js
@@ -12,6 +12,7 @@ export class AttendeeCalendarCommonPopover extends CalendarCommonPopover {
super.setup();
this.user = useService("user");
this.orm = useService("orm");
+ this.actionService = useService("action");
this.askRecurrenceUpdatePolicy = useAskRecurrenceUpdatePolicy();
onWillStart(this.onWillStart);
@@ -67,6 +68,15 @@ export class AttendeeCalendarCommonPopover extends CalendarCommonPopover {
get isEventArchivable() {
return false;
}
+
+ async onClickOpenRecord() {
+ const action = await this.orm.call(
+ 'calendar.event',
+ 'action_open_calendar_event',
+ [this.props.record.id]
+ );
+ this.actionService.doAction(action);
+ }
/**
* @override
diff --git a/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.xml b/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.xml
index 44243f47dc4..f069e13da56 100644
--- a/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.xml
+++ b/addons/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.xml
@@ -4,6 +4,16 @@
isEventDetailsVisible
+
+ !fieldInfo.options.shouldOpenRecord
+
+
+
+
+
+
+
+
diff --git a/addons/calendar/static/tests/attendee_calendar_views_tests.js b/addons/calendar/static/tests/attendee_calendar_views_tests.js
new file mode 100644
index 00000000000..3c3420c0fbb
--- /dev/null
+++ b/addons/calendar/static/tests/attendee_calendar_views_tests.js
@@ -0,0 +1,242 @@
+/** @odoo-module **/
+
+import {
+ getFixture,
+ patchDate,
+ patchWithCleanup,
+} from "@web/../tests/helpers/utils";
+
+import {
+ changeScale,
+ clickEvent,
+} from "@web/../tests/views/calendar/helpers";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { browser } from "@web/core/browser/browser";
+import { registry } from "@web/core/registry";
+import { userService } from "@web/core/user_service";
+const serviceRegistry = registry.category("services");
+
+let target;
+let serverData;
+const uid = -1;
+
+QUnit.module("CalendarView", ({ beforeEach }) => {
+ beforeEach(() => {
+ // 2016-12-12 08:00:00
+ patchDate(2016, 11, 12, 8, 0, 0);
+
+ patchWithCleanup(browser, {
+ setTimeout: (fn) => fn(),
+ clearTimeout: () => { },
+ });
+
+ target = getFixture();
+ setupViewRegistries();
+
+ serviceRegistry.add(
+ "user",
+ {
+ ...userService,
+ start() {
+ const fakeUserService = userService.start(...arguments);
+ Object.defineProperty(fakeUserService, "userId", {
+ get: () => uid,
+ });
+ return fakeUserService;
+ },
+ },
+ { force: true }
+ );
+
+ serverData = {
+ models: {
+ event: {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ user_id: {
+ string: "user",
+ type: "many2one",
+ relation: "user",
+ default: uid,
+ },
+ partner_id: {
+ string: "user",
+ type: "many2one",
+ relation: "partner",
+ related: "user_id.partner_id",
+ default: 1,
+ },
+ name: { string: "name", type: "char" },
+ start_date: { string: "start date", type: "date" },
+ stop_date: { string: "stop date", type: "date" },
+ start: { string: "start datetime", type: "datetime" },
+ stop: { string: "stop datetime", type: "datetime" },
+ delay: { string: "delay", type: "float" },
+ duration: { string: "Duration", type: "float", default: 1 },
+ allday: { string: "allday", type: "boolean" },
+ partner_ids: {
+ string: "attendees",
+ type: "one2many",
+ relation: "partner",
+ default: [[6, 0, [1]]],
+ },
+ type: { string: "type", type: "integer" },
+ event_type_id: {
+ string: "Event Type",
+ type: "many2one",
+ relation: "event_type",
+ },
+ color_event: {
+ string: "Color",
+ type: "integer",
+ related: "event_type_id.color_event_type",
+ },
+ is_hatched: { string: "Hatched", type: "boolean" },
+ is_striked: { string: "Striked", type: "boolean" },
+ res_model_name: { string: "Linked Model Name", type: "char" }
+ },
+ records: [
+ {
+ id: 1,
+ user_id: uid,
+ partner_id: 1,
+ name: "event 1",
+ start: "2016-12-11 00:00:00",
+ stop: "2016-12-11 00:00:00",
+ allday: false,
+ partner_ids: [1, 2, 3],
+ type: 1,
+ is_hatched: false,
+ },
+ {
+ id: 2,
+ user_id: uid,
+ partner_id: 1,
+ name: "event 2",
+ start: "2016-12-12 10:55:05",
+ stop: "2016-12-12 14:55:05",
+ allday: false,
+ partner_ids: [1, 2],
+ type: 3,
+ is_hatched: false,
+ },
+ ],
+ methods: {
+ check_access_rights() {
+ return Promise.resolve(true);
+ }
+ },
+ },
+ user: {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ display_name: { string: "Displayed name", type: "char" },
+ partner_id: {
+ string: "partner",
+ type: "many2one",
+ relation: "partner",
+ },
+ },
+ records: [
+ { id: uid, display_name: "user 1", partner_id: 1 },
+ { id: 4, display_name: "user 4", partner_id: 4 },
+ ],
+ },
+ partner: {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ display_name: { string: "Displayed name", type: "char" },
+ image: { string: "image", type: "integer" },
+ },
+ records: [
+ { id: 1, display_name: "partner 1", image: "AAA" },
+ { id: 2, display_name: "partner 2", image: "BBB" },
+ { id: 3, display_name: "partner 3", image: "CCC" },
+ { id: 4, display_name: "partner 4", image: "DDD" },
+ ],
+ },
+ filter_partner: {
+ fields: {
+ 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: uid, partner_id: 1, partner_checked: true },
+ { id: 2, user_id: uid, partner_id: 2, partner_checked: true },
+ { id: 3, user_id: 4, partner_id: 3, partner_checked: true },
+ ],
+ }
+ }
+ };
+ });
+
+ QUnit.test(`Linked record rendering`, async (assert) => {
+ assert.expect(3);
+
+ serverData.models.event.records.push({
+ id: 3,
+ user_id: 4,
+ partner_id: 1,
+ name: "event With record",
+ start: "2016-12-11 09:00:00",
+ stop: "2016-12-11 10:00:00",
+ allday: false,
+ partner_ids: [1, 2, 3],
+ res_model_name: 'Time Off',
+
+ });
+
+ await makeView({
+ type: "calendar",
+ resModel: "event",
+ serverData,
+ arch: `
+
+
+
+
+
+
+
+
+
+ `,
+ mockRPC(route, args) {
+ if (route === "/web/dataset/call_kw/res.users/has_group") {
+ return Promise.resolve(true);
+ } else if (route === "/web/dataset/call_kw/res.partner/get_attendee_detail") {
+ return Promise.resolve([]);
+ }
+ },
+ });
+ assert.containsOnce(
+ target,
+ ".o_calendar_renderer .fc-view-container",
+ "should instance of fullcalendar"
+ );
+ await changeScale(target, 'week');
+ await clickEvent(target, 3);
+ assert.containsOnce(
+ target,
+ ".fa-link",
+ "A link icon should be present"
+ );
+ assert.strictEqual(target.querySelector("li a[href='#']").textContent, "Time Off");
+ });
+});
diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml
index 5fd4bf88798..e52afaa6c42 100644
--- a/addons/calendar/views/calendar_views.xml
+++ b/addons/calendar/views/calendar_views.xml
@@ -108,10 +108,11 @@
-
+
-
@@ -375,6 +376,8 @@
+