diff --git a/addons/bus/static/tests/helpers/mock_python_environment.js b/addons/bus/static/tests/helpers/mock_python_environment.js
index 7c39e2b00e1..8fa5a7f4061 100644
--- a/addons/bus/static/tests/helpers/mock_python_environment.js
+++ b/addons/bus/static/tests/helpers/mock_python_environment.js
@@ -104,9 +104,6 @@ let pyEnv;
}
pyEnv = new Proxy(
{
- get currentPartner() {
- return this.mockServer.currentPartner;
- },
getData() {
return this.mockServer.models;
},
diff --git a/addons/bus/static/tests/helpers/mock_server.js b/addons/bus/static/tests/helpers/mock_server.js
index 74f0859bfe3..4872fd2ed08 100644
--- a/addons/bus/static/tests/helpers/mock_server.js
+++ b/addons/bus/static/tests/helpers/mock_server.js
@@ -17,7 +17,6 @@ patch(MockServer.prototype, 'bus', {
this._super(message);
},
});
- this.pendingLongpollingPromise = null;
this.notificationsToBeResolved = [];
this.lastBusNotificationId = 0;
},
diff --git a/addons/calendar/static/tests/helpers/mock_server/models/calendar_event.js b/addons/calendar/static/tests/helpers/mock_server/models/calendar_event.js
new file mode 100644
index 00000000000..30ec4379d8a
--- /dev/null
+++ b/addons/calendar/static/tests/helpers/mock_server/models/calendar_event.js
@@ -0,0 +1,17 @@
+/** @odoo-module **/
+
+import { patch } from '@web/core/utils/patch';
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+patch(MockServer.prototype, 'calendar/models/calendar_event', {
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ // calendar.event methods
+ if (args.model === 'calendar.event' && args.method === 'check_access_rights') {
+ return true;
+ }
+ return this._super(...arguments);
+ },
+});
diff --git a/addons/calendar/static/tests/helpers/mock_server/models/mail_activity.js b/addons/calendar/static/tests/helpers/mock_server/models/mail_activity.js
new file mode 100644
index 00000000000..359c82defec
--- /dev/null
+++ b/addons/calendar/static/tests/helpers/mock_server/models/mail_activity.js
@@ -0,0 +1,30 @@
+/** @odoo-module **/
+
+// ensure mail override is applied first.
+import '@mail/../tests/helpers/mock_server/models/mail_activity';
+
+import { patch } from '@web/core/utils/patch';
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+patch(MockServer.prototype, 'calendar/models/mail_activity', {
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ if (args.model === 'mail.activity' && args.method === 'action_create_calendar_event') {
+ return {
+ type: 'ir.actions.act_window',
+ name: "Meetings",
+ res_model: 'calendar.event',
+ view_mode: 'calendar',
+ views: [[false, 'calendar']],
+ target: 'current',
+ };
+ }
+ return this._super(...arguments);
+ },
+});
diff --git a/addons/calendar/static/tests/helpers/mock_server.js b/addons/calendar/static/tests/helpers/mock_server/models/res_users.js
similarity index 64%
rename from addons/calendar/static/tests/helpers/mock_server.js
rename to addons/calendar/static/tests/helpers/mock_server/models/res_users.js
index 0f04b39fd4b..65c4026ba75 100644
--- a/addons/calendar/static/tests/helpers/mock_server.js
+++ b/addons/calendar/static/tests/helpers/mock_server/models/res_users.js
@@ -1,44 +1,14 @@
/** @odoo-module **/
// ensure mail override is applied first.
-import '@mail/../tests/helpers/mock_server';
+import '@mail/../tests/helpers/mock_server/models/res_users';
import { patch } from '@web/core/utils/patch';
import { MockServer } from '@web/../tests/helpers/mock_server';
import { datetime_to_str } from 'web.time';
-patch(MockServer.prototype, 'calendar', {
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- async _performRPC(route, args) {
- // mail.activity methods
- if (args.model === 'mail.activity' && args.method === 'action_create_calendar_event') {
- return {
- type: 'ir.actions.act_window',
- name: "Meetings",
- res_model: 'calendar.event',
- view_mode: 'calendar',
- views: [[false, 'calendar']],
- target: 'current',
- };
- }
- // calendar.event methods
- if (args.model === 'calendar.event' && args.method === 'check_access_rights') {
- return true;
- }
- return this._super(...arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Methods
- //--------------------------------------------------------------------------
-
+patch(MockServer.prototype, 'calendar/models/res_users', {
/**
* Simulates `_systray_get_calendar_event_domain` on `res.users`.
*
diff --git a/addons/hr_holidays/static/tests/helpers/mock_server.js b/addons/hr_holidays/static/tests/helpers/mock_server/models/res_partner.js
similarity index 80%
rename from addons/hr_holidays/static/tests/helpers/mock_server.js
rename to addons/hr_holidays/static/tests/helpers/mock_server/models/res_partner.js
index ec536f54fe8..5b3764f9ad0 100644
--- a/addons/hr_holidays/static/tests/helpers/mock_server.js
+++ b/addons/hr_holidays/static/tests/helpers/mock_server/models/res_partner.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
-import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
+import '@mail/../tests/helpers/mock_server/models/res_partner'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
-patch(MockServer.prototype, 'hr_holidays', {
+patch(MockServer.prototype, 'hr_holidays/models/res_partner', {
/**
* Overrides to add out of office to employees.
*
diff --git a/addons/im_livechat/static/tests/helpers/mock_server.js b/addons/im_livechat/static/tests/helpers/mock_server.js
deleted file mode 100644
index e7109d2ccbf..00000000000
--- a/addons/im_livechat/static/tests/helpers/mock_server.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/** @odoo-module **/
-
-import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
-
-import { patch } from "@web/core/utils/patch";
-import { MockServer } from "@web/../tests/helpers/mock_server";
-
-patch(MockServer.prototype, 'im_livechat', {
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- async _performRPC(route, args) {
- if (route === '/im_livechat/get_session') {
- const channel_id = args.channel_id;
- const anonymous_name = args.anonymous_name;
- const previous_operator_id = args.previous_operator_id;
- const context = args.context;
- return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context);
- }
- return this._super(...arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Routes
- //--------------------------------------------------------------------------
-
- /**
- * Simulates the `/im_livechat/get_session` route.
- *
- * @private
- * @param {integer} channel_id
- * @param {string} anonymous_name
- * @param {integer} [previous_operator_id]
- * @param {Object} [context={}]
- * @returns {Object}
- */
- _mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) {
- let user_id;
- let country_id;
- if ('mockedUserId' in context) {
- // can be falsy to simulate not being logged in
- user_id = context.mockedUserId;
- } else {
- user_id = this.currentUserId;
- }
- // don't use the anonymous name if the user is logged in
- if (user_id) {
- const user = this.getRecords('res.users', [['id', '=', user_id]])[0];
- country_id = user.country_id;
- } else {
- // simulate geoip
- const countryCode = context.mockedCountryCode;
- const country = this.getRecords('res.country', [['code', '=', countryCode]])[0];
- if (country) {
- country_id = country.id;
- anonymous_name = anonymous_name + ' (' + country.name + ')';
- }
- }
- return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id);
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Methods
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _mockMailChannelChannelInfo(ids) {
- const channelInfos = this._super(...arguments);
- for (const channelInfo of channelInfos) {
- const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
- channelInfo['channel']['anonymous_name'] = channel.anonymous_name;
- // add the last message date
- if (channel.channel_type === 'livechat') {
- // add the operator id
- if (channel.livechat_operator_id) {
- const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0];
- // livechat_username ignored for simplicity
- channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')];
- }
- }
- }
- return channelInfos;
- },
- /**
- * Simulates `_get_available_users` on `im_livechat.channel`.
- *
- * @private
- * @param {integer} id
- * @returns {Object}
- */
- _mockImLivechatChannel_getAvailableUsers(id) {
- const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0];
- const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]);
- return users.filter(user => user.im_status === 'online');
- },
- /**
- * Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`.
- *
- * @private
- * @param {integer} id
- * @returns {Object}
- */
- _mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) {
- // partner to add to the mail.channel
- const operator_partner_id = operator.partner_id;
- const membersToAdd = [[0, 0, {
- is_pinned: false,
- partner_id: operator_partner_id,
- }]];
- let visitor_user;
- if (user_id) {
- const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0];
- if (visitor_user && visitor_user.active && visitor_user !== operator) {
- // valid session user (not public)
- membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]);
- }
- } else {
- membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
- }
- const membersName = [
- visitor_user ? visitor_user.display_name : anonymous_name,
- operator.livechat_username ? operator.livechat_username : operator.name,
- ];
- return {
- 'channel_member_ids': membersToAdd,
- 'livechat_active': true,
- 'livechat_operator_id': operator_partner_id,
- 'livechat_channel_id': id,
- 'anonymous_name': user_id ? false : anonymous_name,
- 'country_id': country_id,
- 'channel_type': 'livechat',
- 'name': membersName.join(' '),
- };
- },
- /**
- * Simulates `_get_random_operator` on `im_livechat.channel`.
- * Simplified mock implementation: returns the first available operator.
- *
- * @private
- * @param {integer} id
- * @returns {Object}
- */
- _mockImLivechatChannel_getRandomOperator(id) {
- const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
- return availableUsers[0];
- },
- /**
- * Simulates `_open_livechat_mail_channel` on `im_livechat.channel`.
- *
- * @private
- * @param {integer} id
- * @param {string} anonymous_name
- * @param {integer} [previous_operator_id]
- * @param {integer} [user_id]
- * @param {integer} [country_id]
- * @returns {Object}
- */
- _mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) {
- let operator;
- if (previous_operator_id) {
- const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
- operator = availableUsers.find(user => user.partner_id === previous_operator_id);
- }
- if (!operator) {
- operator = this._mockImLivechatChannel_getRandomOperator(id);
- }
- if (!operator) {
- // no one available
- return false;
- }
- // create the session, and add the link with the given channel
- const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id);
- const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals);
- this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
- return this._mockMailChannelChannelInfo([mailChannelId])[0];
- },
- /**
- * @override
- */
- _mockResPartner_GetChannelsAsMember(ids) {
- const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
- const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
- const livechats = this.getRecords('mail.channel', [
- ['channel_type', '=', 'livechat'],
- ['channel_member_ids', 'in', members.map(member => member.id)],
- ]);
- return [
- ...this._super(ids),
- ...livechats,
- ];
- },
-});
diff --git a/addons/im_livechat/static/tests/helpers/mock_server/controllers/main.js b/addons/im_livechat/static/tests/helpers/mock_server/controllers/main.js
index 318ee9c01f4..60ec036a2c3 100644
--- a/addons/im_livechat/static/tests/helpers/mock_server/controllers/main.js
+++ b/addons/im_livechat/static/tests/helpers/mock_server/controllers/main.js
@@ -1,7 +1,5 @@
/** @odoo-module **/
-import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
-
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
@@ -10,6 +8,13 @@ patch(MockServer.prototype, 'im_livechat/controllers/main', {
* @override
*/
async _performRPC(route, args) {
+ if (route === '/im_livechat/get_session') {
+ const channel_id = args.channel_id;
+ const anonymous_name = args.anonymous_name;
+ const previous_operator_id = args.previous_operator_id;
+ const context = args.context;
+ return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context);
+ }
if (route === '/im_livechat/notify_typing') {
const uuid = args.uuid;
const is_typing = args.is_typing;
@@ -18,6 +23,40 @@ patch(MockServer.prototype, 'im_livechat/controllers/main', {
}
return this._super(...arguments);
},
+ /**
+ * Simulates the `/im_livechat/get_session` route.
+ *
+ * @private
+ * @param {integer} channel_id
+ * @param {string} anonymous_name
+ * @param {integer} [previous_operator_id]
+ * @param {Object} [context={}]
+ * @returns {Object}
+ */
+ _mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) {
+ let user_id;
+ let country_id;
+ if ('mockedUserId' in context) {
+ // can be falsy to simulate not being logged in
+ user_id = context.mockedUserId;
+ } else {
+ user_id = this.currentUserId;
+ }
+ // don't use the anonymous name if the user is logged in
+ if (user_id) {
+ const user = this.getRecords('res.users', [['id', '=', user_id]])[0];
+ country_id = user.country_id;
+ } else {
+ // simulate geoip
+ const countryCode = context.mockedCountryCode;
+ const country = this.getRecords('res.country', [['code', '=', countryCode]])[0];
+ if (country) {
+ country_id = country.id;
+ anonymous_name = anonymous_name + ' (' + country.name + ')';
+ }
+ }
+ return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id);
+ },
/**
* Simulates the `/im_livechat/notify_typing` route.
*
diff --git a/addons/im_livechat/static/tests/helpers/mock_server/models/im_livechat_channel.js b/addons/im_livechat/static/tests/helpers/mock_server/models/im_livechat_channel.js
new file mode 100644
index 00000000000..11720fc1102
--- /dev/null
+++ b/addons/im_livechat/static/tests/helpers/mock_server/models/im_livechat_channel.js
@@ -0,0 +1,100 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'im_livechat/models/im_livechat_channel', {
+ /**
+ * Simulates `_get_available_users` on `im_livechat.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @returns {Object}
+ */
+ _mockImLivechatChannel_getAvailableUsers(id) {
+ const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0];
+ const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]);
+ return users.filter(user => user.im_status === 'online');
+ },
+ /**
+ * Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @returns {Object}
+ */
+ _mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) {
+ // partner to add to the mail.channel
+ const operator_partner_id = operator.partner_id;
+ const membersToAdd = [[0, 0, {
+ is_pinned: false,
+ partner_id: operator_partner_id,
+ }]];
+ let visitor_user;
+ if (user_id) {
+ const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0];
+ if (visitor_user && visitor_user.active && visitor_user !== operator) {
+ // valid session user (not public)
+ membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]);
+ }
+ } else {
+ membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
+ }
+ const membersName = [
+ visitor_user ? visitor_user.display_name : anonymous_name,
+ operator.livechat_username ? operator.livechat_username : operator.name,
+ ];
+ return {
+ 'channel_member_ids': membersToAdd,
+ 'livechat_active': true,
+ 'livechat_operator_id': operator_partner_id,
+ 'livechat_channel_id': id,
+ 'anonymous_name': user_id ? false : anonymous_name,
+ 'country_id': country_id,
+ 'channel_type': 'livechat',
+ 'name': membersName.join(' '),
+ };
+ },
+ /**
+ * Simulates `_get_random_operator` on `im_livechat.channel`.
+ * Simplified mock implementation: returns the first available operator.
+ *
+ * @private
+ * @param {integer} id
+ * @returns {Object}
+ */
+ _mockImLivechatChannel_getRandomOperator(id) {
+ const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
+ return availableUsers[0];
+ },
+ /**
+ * Simulates `_open_livechat_mail_channel` on `im_livechat.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @param {string} anonymous_name
+ * @param {integer} [previous_operator_id]
+ * @param {integer} [user_id]
+ * @param {integer} [country_id]
+ * @returns {Object}
+ */
+ _mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) {
+ let operator;
+ if (previous_operator_id) {
+ const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
+ operator = availableUsers.find(user => user.partner_id === previous_operator_id);
+ }
+ if (!operator) {
+ operator = this._mockImLivechatChannel_getRandomOperator(id);
+ }
+ if (!operator) {
+ // no one available
+ return false;
+ }
+ // create the session, and add the link with the given channel
+ const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id);
+ const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals);
+ this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
+ return this._mockMailChannelChannelInfo([mailChannelId])[0];
+ },
+});
diff --git a/addons/im_livechat/static/tests/helpers/mock_server/models/mail_channel.js b/addons/im_livechat/static/tests/helpers/mock_server/models/mail_channel.js
new file mode 100644
index 00000000000..87d4977e10b
--- /dev/null
+++ b/addons/im_livechat/static/tests/helpers/mock_server/models/mail_channel.js
@@ -0,0 +1,29 @@
+/** @odoo-module **/
+
+import '@mail/../tests/helpers/mock_server/models/mail_channel'; // ensure mail overrides are applied first
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'im_livechat/models/mail_channel', {
+ /**
+ * @override
+ */
+ _mockMailChannelChannelInfo(ids) {
+ const channelInfos = this._super(...arguments);
+ for (const channelInfo of channelInfos) {
+ const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
+ channelInfo['channel']['anonymous_name'] = channel.anonymous_name;
+ // add the last message date
+ if (channel.channel_type === 'livechat') {
+ // add the operator id
+ if (channel.livechat_operator_id) {
+ const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0];
+ // livechat_username ignored for simplicity
+ channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')];
+ }
+ }
+ }
+ return channelInfos;
+ },
+});
diff --git a/addons/im_livechat/static/tests/helpers/mock_server/models/res_partner.js b/addons/im_livechat/static/tests/helpers/mock_server/models/res_partner.js
new file mode 100644
index 00000000000..4f3466b721a
--- /dev/null
+++ b/addons/im_livechat/static/tests/helpers/mock_server/models/res_partner.js
@@ -0,0 +1,24 @@
+/** @odoo-module **/
+
+import '@mail/../tests/helpers/mock_server/models/res_partner'; // ensure mail overrides are applied first
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'im_livechat/models/res_partner', {
+ /**
+ * @override
+ */
+ _mockResPartner_GetChannelsAsMember(ids) {
+ const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
+ const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
+ const livechats = this.getRecords('mail.channel', [
+ ['channel_type', '=', 'livechat'],
+ ['channel_member_ids', 'in', members.map(member => member.id)],
+ ]);
+ return [
+ ...this._super(ids),
+ ...livechats,
+ ];
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
deleted file mode 100644
index c87da3f4b8e..00000000000
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ /dev/null
@@ -1,2576 +0,0 @@
-/** @odoo-module **/
-
-// ensure bus mock server is loaded first.
-import '@bus/../tests/helpers/mock_server';
-
-import { patch } from "@web/core/utils/patch";
-import { MockServer } from "@web/../tests/helpers/mock_server";
-
-import { date_to_str, datetime_to_str } from 'web.time';
-
-
-patch(MockServer.prototype, 'mail', {
- init({ models }) {
- this._super(...arguments);
-
- if (this.currentPartnerId && models && 'res.partner' in models) {
- this.currentPartner = this.getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0];
- }
- // creation of the ir.model.fields records, required for tracked fields
- for (const modelName in models) {
- const fieldNamesToFields = models[modelName].fields;
- for (const fname in fieldNamesToFields) {
- if (fieldNamesToFields[fname].tracking) {
- this.mockCreate('ir.model.fields', { model: modelName, name: fname });
- }
- }
- }
- },
- /**
- * @override
- */
- async performRPC(route, args) {
- if (route === '/mail/attachment/upload') {
- const ufile = args.body.get('ufile');
- const is_pending = args.body.get('is_pending') === 'true';
- const model = is_pending ? 'mail.compose.message' : args.body.get('thread_model');
- const id = is_pending ? 0 : parseInt(args.body.get('thread_id'));
- const attachmentId = this.mockCreate('ir.attachment', {
- // datas,
- mimetype: ufile.type,
- name: ufile.name,
- res_id: id,
- res_model: model,
- });
- const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0];
- return {
- 'filename': attachment.name,
- 'id': attachment.id,
- 'mimetype': attachment.mimetype,
- 'size': attachment.file_size
- };
- }
- return this._super(...arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- async _performRPC(route, args) {
- // routes
- if (route === '/mail/message/post') {
- if (args.thread_model === 'mail.channel') {
- return this._mockMailChannelMessagePost(args.thread_id, args.post_data, args.context);
- }
- return this._mockMailThreadMessagePost(args.thread_model, [args.thread_id], args.post_data, args.context);
- }
- if (route === '/mail/attachment/delete') {
- const { attachment_id } = args;
- return this._mockRouteMailAttachmentRemove(attachment_id);
- }
- if (route === '/mail/chat_post') {
- const uuid = args.uuid;
- const message_content = args.message_content;
- const context = args.context;
- return this._mockRouteMailChatPost(uuid, message_content, context);
- }
- if (route === '/mail/init_messaging') {
- return this._mockRouteMailInitMessaging();
- }
- if (route === '/mail/link_preview') {
- return this._mockRouteMailLinkPreview(args.message_id);
- }
- if (route === '/mail/load_message_failures') {
- return this._mockRouteMailLoadMessageFailures();
- }
- if (route === '/mail/history/messages') {
- const { min_id, max_id, limit } = args;
- return this._mockRouteMailMessageHistory(min_id, max_id, limit);
- }
- if (route === '/mail/inbox/messages') {
- const { min_id, max_id, limit } = args;
- return this._mockRouteMailMessageInbox(min_id, max_id, limit);
- }
- if (route === '/mail/starred/messages') {
- const { min_id, max_id, limit } = args;
- return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit);
- }
- if (route === '/mail/read_subscription_data') {
- const follower_id = args.follower_id;
- return this._mockRouteMailReadSubscriptionData(follower_id);
- }
- if (route === '/mail/thread/data') {
- return this._mockRouteMailThreadData(args.thread_model, args.thread_id, args.request_list);
- }
- if (route === '/mail/thread/messages') {
- const { min_id, max_id, limit, thread_model, thread_id } = args;
- return this._mockRouteMailThreadFetchMessages(thread_model, thread_id, max_id, min_id, limit);
- }
- if (route === '/mail/channel/messages') {
- const { channel_id, min_id, max_id, limit } = args;
- return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit);
- }
- if (new RegExp('/mail/channel/\\d+/partner/\\d+/avatar_128').test(route)) {
- return;
- }
- if (args.model === 'ir.attachment' && args.method === 'register_as_main_attachment') {
- const ids = args.args[0];
- return this._mockIrAttachmentRegisterAsMainAttachment(ids);
- }
- // mail.activity methods
- if (args.model === 'mail.activity' && args.method === 'action_feedback') {
- const ids = args.args[0];
- return this._mockMailActivityActionFeedback(ids);
- }
- if (args.model === 'mail.activity' && args.method === 'action_feedback_schedule_next') {
- const ids = args.args[0];
- return this._mockMailActivityActionFeedbackScheduleNext(ids);
- }
- if (args.model === 'mail.activity' && args.method === 'activity_format') {
- const ids = args.args[0];
- return this._mockMailActivityActivityFormat(ids);
- }
- if (args.model === 'mail.activity' && args.method === 'get_activity_data') {
- const res_model = args.args[0] || args.kwargs.res_model;
- const domain = args.args[1] || args.kwargs.domain;
- return this._mockMailActivityGetActivityData(res_model, domain);
- }
- // mail.channel methods
- if (args.model === 'mail.channel' && args.method === 'action_unfollow') {
- const ids = args.args[0];
- return this._mockMailChannelActionUnfollow(ids);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_fetched') {
- const ids = args.args[0];
- return this._mockMailChannelChannelFetched(ids);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_fetch_preview') {
- const ids = args.args[0];
- return this._mockMailChannelChannelFetchPreview(ids);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_fold') {
- const ids = args.args[0];
- const state = args.args[1] || args.kwargs.state;
- return this._mockMailChannelChannelFold(ids, state);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_get') {
- const partners_to = args.args[0] || args.kwargs.partners_to;
- const pin = args.args[1] !== undefined
- ? args.args[1]
- : args.kwargs.pin !== undefined
- ? args.kwargs.pin
- : undefined;
- return this._mockMailChannelChannelGet(partners_to, pin);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_info') {
- const ids = args.args[0];
- return this._mockMailChannelChannelInfo(ids);
- }
- if (args.model === 'mail.channel' && args.method === 'add_members') {
- const ids = args.args[0];
- const partner_ids = args.args[1] || args.kwargs.partner_ids;
- return this._mockMailChannelAddMembers(ids, partner_ids);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_pin') {
- const ids = args.args[0];
- const pinned = args.args[1] || args.kwargs.pinned;
- return this._mockMailChannelChannelPin(ids, pinned);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_rename') {
- const ids = args.args[0];
- const name = args.args[1] || args.kwargs.name;
- return this._mockMailChannelChannelRename(ids, name);
- }
- if (route === '/mail/channel/set_last_seen_message') {
- const id = args.channel_id;
- const last_message_id = args.last_message_id;
- return this._mockMailChannel_ChannelSeen([id], last_message_id);
- }
- if (args.model === 'mail.channel' && args.method === 'channel_set_custom_name') {
- const ids = args.args[0];
- const name = args.args[1] || args.kwargs.name;
- return this._mockMailChannelChannelSetCustomName(ids, name);
- }
- if (args.model === 'mail.channel' && args.method === 'create_group') {
- const partners_to = args.args[0] || args.kwargs.partners_to;
- return this._mockMailChannelCreateGroup(partners_to);
- }
- if (args.model === 'mail.channel' && args.method === 'execute_command_leave') {
- return this._mockMailChannelExecuteCommandLeave(args);
- }
- if (args.model === 'mail.channel' && args.method === 'execute_command_who') {
- return this._mockMailChannelExecuteCommandWho(args);
- }
- if (args.model === 'mail.channel' && args.method === 'write' && 'image_128' in args.args[1]) {
- const ids = args.args[0];
- return this._mockMailChannelWriteImage128(ids[0]);
- }
- if (args.model === 'mail.channel' && args.method === 'load_more_members') {
- const [channel_ids] = args.args;
- const { known_member_ids } = args.kwargs;
- return this._mockMailChannelLoadMoreMembers(channel_ids, known_member_ids);
- }
- // mail.message methods
- if (args.model === 'mail.message' && args.method === 'mark_all_as_read') {
- const domain = args.args[0] || args.kwargs.domain;
- return this._mockMailMessageMarkAllAsRead(domain);
- }
- if (args.model === 'mail.message' && args.method === 'message_format') {
- const ids = args.args[0];
- return this._mockMailMessageMessageFormat(ids);
- }
- if (args.model === 'mail.message' && args.method === 'set_message_done') {
- const ids = args.args[0];
- return this._mockMailMessageSetMessageDone(ids);
- }
- if (args.model === 'mail.message' && args.method === 'toggle_message_starred') {
- const ids = args.args[0];
- return this._mockMailMessageToggleMessageStarred(ids);
- }
- if (args.model === 'mail.message' && args.method === 'unstar_all') {
- return this._mockMailMessageUnstarAll();
- }
- if (args.model === 'res.users.settings' && args.method === '_find_or_create_for_user') {
- const user_id = args.args[0][0];
- return this._mockResUsersSettings_FindOrCreateForUser(user_id);
- }
- if (args.model === 'res.users.settings' && args.method === 'set_res_users_settings') {
- const id = args.args[0][0];
- const newSettings = args.kwargs.new_settings;
- return this._mockResUsersSettingsSetResUsersSettings(id, newSettings);
- }
- // res.partner methods
- if (args.method === 'get_mention_suggestions') {
- if (args.model === 'mail.channel') {
- return this._mockMailChannelGetMentionSuggestions(args);
- }
- if (args.model === 'res.partner') {
- return this._mockResPartnerGetMentionSuggestions(args);
- }
- }
- if (args.model === 'res.partner' && args.method === 'im_search') {
- const name = args.args[0] || args.kwargs.search;
- const limit = args.args[1] || args.kwargs.limit;
- return this._mockResPartnerImSearch(name, limit);
- }
- if (args.model === 'res.partner' && args.method === 'search_for_channel_invite') {
- const search_term = args.args[0] || args.kwargs.search_term;
- const channel_id = args.args[1] || args.kwargs.channel_id;
- const limit = args.args[2] || args.kwargs.limit;
- return this._mockResPartnerSearchForChannelInvite(search_term, channel_id, limit);
- }
- // res.users method
- if (args.model === 'res.users' && args.method === 'systray_get_activities') {
- return this._mockResUsersSystrayGetActivities();
- }
- // mail.thread methods (can work on any model)
- if (args.method === 'message_subscribe') {
- const ids = args.args[0];
- const partner_ids = args.args[1] || args.kwargs.partner_ids;
- const subtype_ids = args.args[2] || args.kwargs.subtype_ids;
- return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, subtype_ids);
- }
- if (args.method === 'message_unsubscribe') {
- const ids = args.args[0];
- const partner_ids = args.args[1] || args.kwargs.partner_ids;
- return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids);
- }
- if (args.method === 'message_post') {
- const id = args.args[0];
- const kwargs = args.kwargs;
- const context = kwargs.context;
- delete kwargs.context;
- return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
- }
- if (args.method === 'notify_cancel_by_type') {
- return this._mockMailThreadNotifyCancelByType(args.model, args.kwargs.notification_type);
- }
- return this._super(route, args);
- },
- /**
- * @override
- */
- mockWrite(model) {
- const initialTrackedFieldValuesByRecordId = this._mockMailThread_TrackPrepare(model);
- const mockWriteResult = this._super(...arguments);
- if (initialTrackedFieldValuesByRecordId) {
- this._mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId);
- }
- return mockWriteResult;
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Routes
- //--------------------------------------------------------------------------
-
- /**
- * Simulates `activity_format` on `mail.activity`.
- *
- * @private
- * @param {number[]} ids
- * @returns {Object[]}
- */
- _mockMailActivityActivityFormat(ids) {
- let res = this.mockRead('mail.activity', [ids]);
- res = res.map(record => {
- if (record.mail_template_ids) {
- record.mail_template_ids = record.mail_template_ids.map(template_id => {
- const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
- return {
- id: template.id,
- name: template.name,
- };
- });
- }
- return record;
- });
- return res;
- },
-
- /**
- * Simulates the `/mail/attachment/delete` route.
- *
- * @private
- * @param {integer} attachment_id
- */
- async _mockRouteMailAttachmentRemove(attachment_id) {
- this.pyEnv['bus.bus']._sendone(this.currentPartnerId, 'ir.attachment/delete', { id: attachment_id });
- return this.pyEnv['ir.attachment'].unlink([attachment_id]);
- },
-
- /**
- * Simulates the `/mail/channel/messages` route.
- *
- * @private
- * @param {integer} channel_id
- * @param {integer} limit
- * @param {integer} max_id
- * @param {integer} min_id
- * @returns {Object} list of messages
- */
- async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) {
- const domain = [
- ['res_id', '=', channel_id],
- ['model', '=', 'mail.channel'],
- ['message_type', '!=', 'user_notification'],
- ];
- const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
- this._mockMailMessageSetMessageDone(messages.map(message => message.id));
- return this._mockMailMessageMessageFormat(messages.map(message => message.id));
- },
- /**
- * Simulates the `/mail/chat_post` route.
- *
- * @private
- * @param {string} uuid
- * @param {string} message_content
- * @param {Object} [context={}]
- * @returns {Object} one key for list of followers and one for subtypes
- */
- async _mockRouteMailChatPost(uuid, message_content, context = {}) {
- const mailChannel = this.getRecords('mail.channel', [['uuid', '=', uuid]])[0];
- if (!mailChannel) {
- return false;
- }
-
- let user_id;
- // find the author from the user session
- if ('mockedUserId' in context) {
- // can be falsy to simulate not being logged in
- user_id = context.mockedUserId;
- } else {
- user_id = this.currentUserId;
- }
- let author_id;
- let email_from;
- if (user_id) {
- const author = this.getRecords('res.users', [['id', '=', user_id]])[0];
- author_id = author.partner_id;
- email_from = `${author.display_name} <${author.email}>`;
- } else {
- author_id = false;
- // simpler fallback than catchall_formatted
- email_from = mailChannel.anonymous_name || "catchall@example.com";
- }
- // supposedly should convert plain text to html
- const body = message_content;
- // ideally should be posted with mail_create_nosubscribe=True
- return this._mockMailChannelMessagePost(
- mailChannel.id,
- {
- author_id,
- email_from,
- body,
- message_type: 'comment',
- subtype_xmlid: 'mail.mt_comment',
- },
- context
- );
- },
- /**
- * Simulates the `/mail/init_messaging` route.
- *
- * @private
- * @returns {Object}
- */
- _mockRouteMailInitMessaging() {
- return this._mockResUsers_InitMessaging([this.currentUserId]);
- },
- /**
- * Simulates `/mail/link_preview` route.
- *
- * @private
- * @param {integer} message_id
- * @returns {Object}
- */
- _mockRouteMailLinkPreview(message_id) {
- const linkPreviews = [];
- const [message] = this.pyEnv['mail.message'].searchRead([['id', '=', message_id]]);
- if (message.body === 'https://make-link-preview.com') {
- const linkPreviewId = this.pyEnv['mail.link.preview'].create({
- og_description: "test description",
- og_title: 'Article title',
- og_type: 'article',
- source_url: 'https://make-link-preview.com',
- });
- const [linkPreview] = this.pyEnv['mail.link.preview'].searchRead([['id', '=', linkPreviewId]]);
- linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview));
- let target = this.currentPartner;
- if (message.model === 'mail.channel') {
- target = this.pyEnv['mail.channel'].search([['id', '=', message.res_id]]);
- }
- this.pyEnv['bus.bus']._sendmany([[target, 'mail.link.preview/insert', linkPreviews]]);
- }
- },
- /**
- * Simulates the `/mail/load_message_failures` route.
- *
- * @private
- * @returns {Object[]}
- */
- _mockRouteMailLoadMessageFailures() {
- return this._mockResPartner_MessageFetchFailed(this.currentPartnerId);
- },
- /**
- * Simulates the `/mail/history/messages` route.
- *
- * @private
- * @returns {Object}
- */
- _mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) {
- const domain = [['needaction', '=', false]];
- const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
- return this._mockMailMessageMessageFormat(messages.map(message => message.id));
- },
- /**
- * Simulates the `/mail/inbox/messages` route.
- *
- * @private
- * @returns {Object}
- */
- _mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) {
- const domain = [['needaction', '=', true]];
- const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
- return this._mockMailMessageMessageFormat(messages.map(message => message.id));
- },
- /**
- * Simulates the `/mail/starred/messages` route.
- *
- * @private
- * @returns {Object}
- */
- _mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) {
- const domain = [['starred_partner_ids', 'in', [this.currentPartnerId]]];
- const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
- return this._mockMailMessageMessageFormat(messages.map(message => message.id));
- },
- /**
- * Simulates the `/mail/read_subscription_data` route.
- *
- * @private
- * @param {integer} follower_id
- * @returns {Object[]} list of followed subtypes
- */
- async _mockRouteMailReadSubscriptionData(follower_id) {
- const follower = this.getRecords('mail.followers', [['id', '=', follower_id]])[0];
- const subtypes = this.getRecords('mail.message.subtype', [
- '&',
- ['hidden', '=', false],
- '|',
- ['res_model', '=', follower.res_model],
- ['res_model', '=', false],
- ]);
- const subtypes_list = subtypes.map(subtype => {
- const parent = this.getRecords('mail.message.subtype', [
- ['id', '=', subtype.parent_id],
- ])[0];
- return {
- 'default': subtype.default,
- 'followed': follower.subtype_ids.includes(subtype.id),
- 'id': subtype.id,
- 'internal': subtype.internal,
- 'name': subtype.name,
- 'parent_model': parent ? parent.res_model : false,
- 'res_model': subtype.res_model,
- 'sequence': subtype.sequence,
- };
- });
- // NOTE: server is also doing a sort here, not reproduced for simplicity
- return subtypes_list;
- },
- /**
- * Simulates the `/mail/thread/data` route.
- *
- * @param {string} thread_model
- * @param {integer} thread_id
- * @param {string[]} request_list
- * @returns {Object}
- */
- async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
- const res = {
- 'hasWriteAccess': true, // mimic user with write access by default
- 'hasReadAccess': true,
- };
- const thread = this.pyEnv[thread_model].searchRead([['id', '=', thread_id]])[0];
- if (!thread) {
- res['hasReadAccess'] = false;
- return res;
- }
- if (request_list.includes('activities')) {
- const activities = this.pyEnv['mail.activity'].searchRead([['id', 'in', thread.activity_ids || []]]);
- res['activities'] = this._mockMailActivityActivityFormat(activities.map(activity => activity.id));
- }
- if (request_list.includes('attachments')) {
- const attachments = this.pyEnv['ir.attachment'].searchRead(
- [['res_id', '=', thread.id], ['res_model', '=', thread_model]],
- ); // order not done for simplicity
- res['attachments'] = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
- res['mainAttachment'] = thread.message_main_attachment_id ? { 'id': thread.message_main_attachment_id[0] } : [['clear']];
- }
- if (request_list.includes('followers')) {
- const followers = this.pyEnv['mail.followers'].searchRead([['id', 'in', thread.message_follower_ids || []]]);
- // search read returns many2one relations as an array [id, display_name].
- // But the original route does not. Thus, we need to change it now.
- followers.forEach(follower => follower.partner_id = follower.partner_id[0]);
- res['followers'] = followers;
- }
- if (request_list.includes('suggestedRecipients')) {
- res['suggestedRecipients'] = this._mockMailThread_MessageGetSuggestedRecipients(thread_model, [thread.id])[thread_id];
- }
- return res;
- },
- /**
- * Simulates the `/mail/thread/messages` route.
- *
- * @private
- * @param {string} res_model
- * @param {integer} res_id
- * @param {integer} max_id
- * @param {integer} min_id
- * @param {integer} limit
- * @returns {Object[]} list of messages
- */
- async _mockRouteMailThreadFetchMessages(res_model, res_id, max_id = false, min_id = false, limit = 30) {
- const domain = [
- ['res_id', '=', res_id],
- ['model', '=', res_model],
- ['message_type', '!=', 'user_notification'],
- ];
- const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
- this._mockMailMessageSetMessageDone(messages.map(message => message.id));
- return this._mockMailMessageMessageFormat(messages.map(message => message.id));
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Methods
- //--------------------------------------------------------------------------
-
- /**
- * Simulates `_attachment_format` on `ir.attachment`.
- *
- * @private
- * @param {integer} ids
- * @returns {Object}
- */
- _mockIrAttachment_attachmentFormat(ids) {
- const attachments = this.mockRead('ir.attachment', [ids]);
- return attachments.map(attachment => {
- const res = {
- 'checksum': attachment.checksum,
- 'filename': attachment.name,
- 'id': attachment.id,
- 'mimetype': attachment.mimetype,
- 'name': attachment.name,
- };
- res['originThread'] = [['insert', {
- 'id': attachment.res_id,
- 'model': attachment.res_model,
- }]];
- return res;
- });
- },
- /**
- * Simulates `register_as_main_attachment` on `ir.attachment`.
- *
- * @private
- * @param {integer} ids
- * @param {boolean} [force=true]
- * @returns {boolean} dummy value for mock server
- */
- _mockIrAttachmentRegisterAsMainAttachment(ids, force = true) {
- const [attachment] = this.getRecords('ir.attachment', [['id', 'in', ids]]);
- if (!attachment.res_model) {
- return true; // dummy value for mock server
- }
- if (!this.models[attachment.res_model].fields['message_main_attachment_id']) {
- return true; // dummy value for mock server
- }
- const [record] = this.pyEnv[attachment.res_model].searchRead([['id', '=', attachment.res_id]]);
- if (force || !record.message_main_attachment_id) {
- this.pyEnv[attachment.res_model].write(
- [record.id],
- { message_main_attachment_id: attachment.id },
- );
- }
- return true; // dummy value for mock server
- },
- /**
- * Simulates `_action_done` on `mail.activity`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockMailActivityActionDone(ids) {
- const activities = this.getRecords('mail.activity', [['id', 'in', ids]]);
- this.mockUnlink('mail.activity', [activities.map(activity => activity.id)]);
- },
- /**
- * Simulates `action_feedback` on `mail.activity`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockMailActivityActionFeedback(ids) {
- this._mockMailActivityActionDone(ids);
- },
- /**
- * Simulates `action_feedback_schedule_next` on `mail.activity`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockMailActivityActionFeedbackScheduleNext(ids) {
- this._mockMailActivityActionDone(ids);
- return {
- name: 'Schedule an Activity',
- view_mode: 'form',
- res_model: 'mail.activity',
- views: [[false, 'form']],
- type: 'ir.actions.act_window',
- };
- },
- /**
- * Simulates `get_activity_data` on `mail.activity`.
- *
- * @private
- * @param {string} res_model
- * @param {string} domain
- * @returns {Object}
- */
- _mockMailActivityGetActivityData(res_model, domain) {
- const self = this;
- const records = this.getRecords(res_model, domain);
-
- const activityTypes = this.getRecords('mail.activity.type', []);
- const activityIds = _.pluck(records, 'activity_ids').flat();
-
- const groupedActivities = {};
- const resIdToDeadline = {};
- const groups = self.mockReadGroup('mail.activity', {
- domain: [['id', 'in', activityIds]],
- fields: ['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
- groupby: ['res_id', 'activity_type_id'],
- lazy: false,
- });
- groups.forEach(function (group) {
- // mockReadGroup doesn't correctly return all asked fields
- const activites = self.getRecords('mail.activity', group.__domain);
- group.activity_type_id = group.activity_type_id[0];
- let minDate;
- activites.forEach(function (activity) {
- if (!minDate || moment(activity.date_deadline) < moment(minDate)) {
- minDate = activity.date_deadline;
- }
- });
- group.date_deadline = minDate;
- resIdToDeadline[group.res_id] = minDate;
- let state;
- if (group.date_deadline === moment().format("YYYY-MM-DD")) {
- state = 'today';
- } else if (moment(group.date_deadline) > moment()) {
- state = 'planned';
- } else {
- state = 'overdue';
- }
- if (!groupedActivities[group.res_id]) {
- groupedActivities[group.res_id] = {};
- }
- groupedActivities[group.res_id][group.activity_type_id] = {
- count: group.__count,
- state: state,
- o_closest_deadline: group.date_deadline,
- ids: _.pluck(activites, 'id'),
- };
- });
-
- return {
- activity_types: activityTypes.map((type) => {
- let mailTemplates = [];
- if (type.mail_template_ids) {
- mailTemplates = type.mail_template_ids.map((template_id) => {
- const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
- return {
- id: template.id,
- name: template.name,
- };
- });
- }
- return [type.id, type.display_name, mailTemplates];
- }),
- activity_res_ids: _.sortBy(_.pluck(records, 'id'), function (id) {
- return moment(resIdToDeadline[id]);
- }),
- grouped_activities: groupedActivities,
- };
- },
- _mockMailBaseModel_MailTrack(model, trackedFieldNamesToField, initialTrackedFieldValues, record) {
- const trackingValueIds = [];
- const changedFieldNames = [];
- for (const fname in trackedFieldNamesToField) {
- const initialValue = initialTrackedFieldValues[fname];
- const newValue = record[fname];
- if (initialValue !== newValue) {
- const tracking = this._mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fname, trackedFieldNamesToField[fname], model);
- if (tracking) {
- trackingValueIds.push(tracking);
- }
- changedFieldNames.push(fname);
- }
- }
- return { changedFieldNames, trackingValueIds };
- },
- /**
- * Simulates `action_unfollow` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- */
- _mockMailChannelActionUnfollow(ids) {
- const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
- const [channelMember] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
- if (!channelMember) {
- return true;
- }
- this.pyEnv['mail.channel'].write(
- [channel.id],
- {
- channel_member_ids: [[2, channelMember.id]],
- },
- );
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.channel/leave', {
- 'id': channel.id,
- });
- /**
- * Leave message not posted here because it would send the new message
- * notification on a separate bus notification list from the unsubscribe
- * itself which would lead to the channel being pinned again (handler
- * for unsubscribe is weak and is relying on both of them to be sent
- * together on the bus).
- */
- // this._mockMailChannelMessagePost(channel.id, {
- // author_id: this.currentPartnerId,
- // body: '
left the channel
',
- // subtype_xmlid: "mail.mt_comment",
- // });
- return true;
- },
- /**
- * Simulates `add_members` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- * @param {integer[]} partner_ids
- */
- _mockMailChannelAddMembers(ids, partner_ids) {
- const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
- const partners = this.getRecords('res.partner', [['id', 'in', partner_ids]]);
- for (const partner of partners) {
- this.pyEnv['mail.channel.member'].create({
- channel_id: channel.id,
- partner_id: partner.id,
- });
- const body = `invited ${partner.name} to the channel
`;
- const message_type = "notification";
- const subtype_xmlid = "mail.mt_comment";
- this._mockMailChannelMessagePost(
- channel.id,
- { body, message_type, subtype_xmlid },
- );
- }
- this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/joined', {
- 'channel': this._mockMailChannelChannelInfo([channel.id])[0],
- 'invited_by_user_id': this.currentUserId,
- });
- },
- /**
- * Simulates `_broadcast` on `mail.channel`.
- *
- * @private
- * @param {integer} id
- * @param {integer[]} partner_ids
- * @returns {Object}
- */
- _mockMailChannel_broadcast(ids, partner_ids) {
- const notifications = this._mockMailChannel_channelChannelNotifications(ids, partner_ids);
- this.pyEnv['bus.bus']._sendmany(notifications);
- },
- /**
- * Simulates `_channel_channel_notifications` on `mail.channel`.
- *
- * @private
- * @param {integer} id
- * @param {integer[]} partner_ids
- * @returns {Object}
- */
- _mockMailChannel_channelChannelNotifications(ids, partner_ids) {
- const notifications = [];
- for (const partner_id of partner_ids) {
- const user = this.getRecords('res.users', [['partner_id', 'in', partner_id]])[0];
- if (!user) {
- continue;
- }
- // Note: `channel_info` on the server is supposed to be called with
- // the proper user context but this is not done here for simplicity.
- const channelInfos = this._mockMailChannelChannelInfo(ids);
- const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', partner_id]]);
- for (const channelInfo of channelInfos) {
- notifications.push([relatedPartner, 'mail.channel/legacy_insert', channelInfo]);
- }
- }
- return notifications;
- },
- /**
- * Simulates `channel_fetched` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- * @param {string} extra_info
- */
- _mockMailChannelChannelFetched(ids) {
- const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
- for (const channel of channels) {
- const channelMessages = this.getRecords('mail.message', [
- ['model', '=', 'mail.channel'],
- ['res_id', '=', channel.id],
- ]);
- const lastMessage = channelMessages.reduce((lastMessage, message) => {
- if (message.id > lastMessage.id) {
- return message;
- }
- return lastMessage;
- }, channelMessages[0]);
- if (!lastMessage) {
- continue;
- }
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
- this.pyEnv['mail.channel.member'].write(
- [memberOfCurrentUser.id],
- { fetched_message_id: lastMessage.id },
- );
- this.pyEnv['bus.bus']._sendone(channel, 'mail.channel.member/fetched', {
- 'channel_id': channel.id,
- 'id': memberOfCurrentUser.id,
- 'last_message_id': lastMessage.id,
- 'partner_id': this.currentPartnerId,
- });
- }
- },
- /**
- * Simulates `channel_fetch_preview` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- * @returns {Object[]} list of channels previews
- */
- _mockMailChannelChannelFetchPreview(ids) {
- const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
- return channels.map(channel => {
- const channelMessages = this.getRecords('mail.message', [
- ['model', '=', 'mail.channel'],
- ['res_id', '=', channel.id],
- ]);
- const lastMessage = channelMessages.reduce((lastMessage, message) => {
- if (message.id > lastMessage.id) {
- return message;
- }
- return lastMessage;
- }, channelMessages[0]);
- return {
- id: channel.id,
- last_message: lastMessage ? this._mockMailMessageMessageFormat([lastMessage.id])[0] : false,
- };
- });
- },
- /**
- * Simulates the 'channel_fold' route on `mail.channel`.
- * In particular sends a notification on the bus.
- *
- * @private
- * @param {number} ids
- * @param {state} [state]
- */
- _mockMailChannelChannelFold(ids, state) {
- const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
- for (const channel of channels) {
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
- const foldState = state ? state : memberOfCurrentUser.fold_state === 'open' ? 'folded' : 'open';
- const vals = {
- fold_state: foldState,
- is_minimized: foldState !== 'closed',
- };
- this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], vals);
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.thread/insert', {
- 'id': channel.id,
- 'model': 'mail.channel',
- 'serverFoldState': memberOfCurrentUser.fold_state,
- });
- }
- },
- /**
- * Simulates 'channel_get' on 'mail.channel'.
- *
- * @private
- * @param {integer[]} [partners_to=[]]
- * @param {boolean} [pin=true]
- * @returns {Object}
- */
- _mockMailChannelChannelGet(partners_to = [], pin = true) {
- if (partners_to.length === 0) {
- return false;
- }
- if (!partners_to.includes(this.currentPartnerId)) {
- partners_to.push(this.currentPartnerId);
- }
- const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
- // NOTE: this mock is not complete, which is done for simplicity.
- // Indeed if a chat already exists for the given partners, the server
- // is supposed to return this existing chat. But the mock is currently
- // always creating a new chat, because no test is relying on receiving
- // an existing chat.
- const id = this.pyEnv['mail.channel'].create({
- channel_member_ids: partners.map(partner => [0, 0, {
- partner_id: partner.id,
- }]),
- channel_type: 'chat',
- name: partners.map(partner => partner.name).join(", "),
- });
- return this._mockMailChannelChannelInfo([id])[0];
- },
- /**
- * Simulates `channel_info` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- * @returns {Object[]}
- */
- _mockMailChannelChannelInfo(ids) {
- const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
- return channels.map(channel => {
- const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
- const messages = this.getRecords('mail.message', [
- ['model', '=', 'mail.channel'],
- ['res_id', '=', channel.id],
- ]);
- const [group_public_id] = this.getRecords('res.groups', [
- ['id', '=', channel.group_public_id],
- ]);
- const lastMessageId = messages.reduce((lastMessageId, message) => {
- if (!lastMessageId || message.id > lastMessageId) {
- return message.id;
- }
- return lastMessageId;
- }, undefined);
- const messageNeedactionCounter = this.getRecords('mail.notification', [
- ['res_partner_id', '=', this.currentPartnerId],
- ['is_read', '=', false],
- ['mail_message_id', 'in', messages.map(message => message.id)],
- ]).length;
- const channelData = {
- avatarCacheKey: channel.avatarCacheKey,
- channel_type: channel.channel_type,
- id: channel.id,
- memberCount: channel.member_count,
- };
- const res = Object.assign({}, channel, {
- last_message_id: lastMessageId,
- message_needaction_counter: messageNeedactionCounter,
- authorizedGroupFullName: group_public_id ? group_public_id.name : false,
- });
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
- if (memberOfCurrentUser) {
- Object.assign(res, {
- is_minimized: memberOfCurrentUser.is_minimized,
- is_pinned: memberOfCurrentUser.is_pinned,
- last_interest_dt: memberOfCurrentUser.last_interest_dt,
- message_unread_counter: memberOfCurrentUser.message_unread_counter,
- state: memberOfCurrentUser.fold_state || 'open',
- });
- Object.assign(channelData, {
- custom_channel_name: memberOfCurrentUser.custom_channel_name,
- serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter,
- });
- if (memberOfCurrentUser.rtc_inviting_session_id) {
- res['rtc_inviting_session'] = { 'id': memberOfCurrentUser.rtc_inviting_session_id };
- }
- channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat([memberOfCurrentUser.id])]];
- }
- if (channel.channel_type !== 'channel') {
- res['seen_partners_info'] = members.filter(member => member.partner_id).map(member => {
- return {
- partner_id: member.partner_id,
- seen_message_id: member.seen_message_id,
- fetched_message_id: member.fetched_message_id,
- };
- });
- channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat(members.map(member => member.id))]];
- }
- res.channel = channelData;
- return res;
- });
- },
- /**
- * Simulates the `channel_pin` method of `mail.channel`.
- *
- * @private
- * @param {number[]} ids
- * @param {boolean} [pinned=false]
- */
- async _mockMailChannelChannelPin(ids, pinned = false) {
- const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId], ['is_pinned', '!=', pinned]]);
- if (memberOfCurrentUser) {
- this.pyEnv['mail.channel.member'].write(
- [memberOfCurrentUser.id],
- { is_pinned: pinned },
- );
- }
- if (!pinned) {
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.channel/unpin', {
- 'id': channel.id,
- });
- } else {
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.channel/legacy_insert', this._mockMailChannelChannelInfo([channel.id])[0]);
- }
- },
- /**
- * Simulates the `_channel_seen` method of `mail.channel`.
- *
- * @private
- * @param integer[] ids
- * @param {integer} last_message_id
- */
- async _mockMailChannel_ChannelSeen(ids, last_message_id) {
- // Update record
- const channel_id = ids[0];
- if (!channel_id) {
- throw new Error('Should only be one channel in channel_seen mock params');
- }
- const channel = this.getRecords('mail.channel', [['id', '=', channel_id]])[0];
- const messagesBeforeGivenLastMessage = this.getRecords('mail.message', [
- ['id', '<=', last_message_id],
- ['model', '=', 'mail.channel'],
- ['res_id', '=', channel.id],
- ]);
- if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) {
- return;
- }
- if (!channel) {
- return;
- }
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', this.currentPartnerId]]);
- if (memberOfCurrentUser.seen_message_id && memberOfCurrentUser.seen_message_id >= last_message_id) {
- return;
- }
- this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id);
- this.pyEnv['bus.bus']._sendone(channel.channel_type === 'chat' ? channel : this.currentPartner, 'mail.channel.member/seen', {
- 'channel_id': channel.id,
- 'last_message_id': last_message_id,
- 'partner_id': this.currentPartnerId,
- });
- },
- /**
- * Simulates `channel_rename` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- */
- _mockMailChannelChannelRename(ids, name) {
- const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
- this.pyEnv['mail.channel'].write(
- [channel.id],
- { name },
- );
- this.pyEnv['bus.bus']._sendone(channel, 'mail.thread/insert', {
- 'id': channel.id,
- 'model': 'mail.channel',
- 'name': name,
- });
- },
- /**
- * Simulates `channel_set_custom_name` on `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- */
- _mockMailChannelChannelSetCustomName(ids, name) {
- const channelId = ids[0]; // simulate ensure_one.
- const [memberIdOfCurrentUser] = this.pyEnv['mail.channel.member'].search([['partner_id', '=', this.currentPartnerId], ['channel_id', '=', channelId]]);
- this.pyEnv['mail.channel.member'].write(
- [memberIdOfCurrentUser],
- { custom_channel_name: name },
- );
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.channel/insert', {
- 'custom_channel_name': name,
- 'id': channelId,
- });
- },
- /**
- * Simulates the `create_group` on `mail.channel`.
- *
- * @private
- * @param {integer[]} partners_to
- * @returns {Object}
- */
- async _mockMailChannelCreateGroup(partners_to) {
- const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
- const id = this.pyEnv['mail.channel'].create({
- channel_type: 'group',
- channel_member_ids: partners.map(partner => [0, 0, { partner_id: partner.id }]),
- name: '',
- });
- this._mockMailChannel_broadcast(id, partners.map(partner => partner.id));
- return this._mockMailChannelChannelInfo([id])[0];
- },
- /**
- * Simulates `execute_command_leave` on `mail.channel`.
- *
- * @private
- */
- _mockMailChannelExecuteCommandLeave(args) {
- const channel = this.getRecords('mail.channel', [['id', 'in', args.args[0]]])[0];
- if (channel.channel_type === 'channel') {
- this._mockMailChannelActionUnfollow([channel.id]);
- } else {
- this._mockMailChannelChannelPin(channel.uuid, false);
- }
- },
- /**
- * Simulates `execute_command_who` on `mail.channel`.
- *
- * @private
- */
- _mockMailChannelExecuteCommandWho(args) {
- const ids = args.args[0];
- const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
- for (const channel of channels) {
- const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
- const otherPartnerIds = members.filter(member => member.partner_id && member.partner_id !== this.currentPartnerId).map(member => member.partner_id);
- const otherPartners = this.getRecords('res.partner', [['id', 'in', otherPartnerIds]]);
- let message = "You are alone in this channel.";
- if (otherPartners.length > 0) {
- message = `Users in this channel: ${otherPartners.map(partner => partner.name).join(', ')} and you`;
- }
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.channel/transient_message', {
- 'body': `${message}`,
- 'model': 'mail.channel',
- 'res_id': channel.id,
- });
- }
- },
- /**
- * Simulates `get_mention_suggestions` on `mail.channel`.
- *
- * @private
- * @returns {Array[]}
- */
- _mockMailChannelGetMentionSuggestions(args) {
- const search = args.kwargs.search || '';
- const limit = args.kwargs.limit || 8;
-
- /**
- * Returns the given list of channels after filtering it according to
- * the logic of the Python method `get_mention_suggestions` for the
- * given search term. The result is truncated to the given limit and
- * formatted as expected by the original method.
- *
- * @param {Object[]} channels
- * @param {string} search
- * @param {integer} limit
- * @returns {Object[]}
- */
- const mentionSuggestionsFilter = function (channels, search, limit) {
- const matchingChannels = channels
- .filter(channel => {
- // no search term is considered as return all
- if (!search) {
- return true;
- }
- // otherwise name or email must match search term
- if (channel.name && channel.name.includes(search)) {
- return true;
- }
- return false;
- }).map(channel => {
- // expected format
- return {
- authorizedGroupFullName: channel.group_public_id ? channel.group_public_id.name : false,
- channel: {
- channel_type: channel.channel_type,
- id: channel.id,
- },
- id: channel.id,
- name: channel.name,
- };
- });
- // reduce results to max limit
- matchingChannels.length = Math.min(matchingChannels.length, limit);
- return matchingChannels;
- };
-
- const mentionSuggestions = mentionSuggestionsFilter(this.models['mail.channel'].records, search, limit);
-
- return mentionSuggestions;
- },
- /**
- * Simulates `write` on `mail.channel` when `image_128` changes.
- *
- * @param {integer} id
- */
- _mockMailChannelWriteImage128(id) {
- this.pyEnv['mail.channel'].write(
- [id],
- {
- avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"),
- },
- );
- const channel = this.pyEnv['mail.channel'].searchRead([['id', '=', id]])[0];
- this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/insert', {
- 'avatarCacheKey': channel.avatarCacheKey,
- 'id': id,
- });
- },
- /**
- * Simulates `message_post` on `mail.channel`.
- *
- * @private
- * @param {integer} id
- * @param {Object} kwargs
- * @param {Object} [context]
- * @returns {integer|false}
- */
- _mockMailChannelMessagePost(id, kwargs, context) {
- const message_type = kwargs.message_type || 'notification';
- const channel = this.getRecords('mail.channel', [['id', '=', id]])[0];
- if (channel.channel_type !== 'channel') {
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
- this.pyEnv['mail.channel.member'].write(
- [memberOfCurrentUser.id],
- {
- last_interest_dt: datetime_to_str(new Date()),
- is_pinned: true,
- },
- );
- }
- const messageData = this._mockMailThreadMessagePost(
- 'mail.channel',
- [id],
- Object.assign(kwargs, {
- message_type,
- }),
- context,
- );
- if (kwargs.author_id === this.currentPartnerId) {
- this._mockMailChannel_SetLastSeenMessage([channel.id], messageData.id);
- }
- // simulate compute of message_unread_counter
- const otherMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '!=', this.currentPartnerId]]);
- for (const member of otherMembers) {
- this.pyEnv['mail.channel.member'].write(
- [member.id],
- { message_unread_counter: member.message_unread_counter + 1 },
- );
- }
- return messageData;
- },
- /**
- * Simulates `load_more_members` on `mail.channel`.
- *
- * @private
- * @param {integer[]} channel_ids
- * @param {integer[]} known_member_ids
- */
- _mockMailChannelLoadMoreMembers(channel_ids, known_member_ids) {
- const members = this.pyEnv['mail.channel.member'].searchRead([
- ['id', 'not in', known_member_ids],
- ['channel_id', 'in', channel_ids],
- ], { limit: 100 });
- const memberCount = this.pyEnv['mail.channel.member'].searchCount([
- ['channel_id', 'in', channel_ids],
- ]);
- const membersData = [];
- for (const member of members) {
- let persona;
- if (member.partner_id) {
- const [partner] = this.pyEnv['res.partner'].searchRead(
- [['id', '=', member.partner_id[0]]],
- { fields: ['id', 'name', 'im_status'] }
- );
- persona = {
- 'partner': {
- 'id': partner.id,
- 'name': partner.name,
- 'im_status': partner.im_status,
- },
- };
- }
- if (member.guest_id) {
- const [guest] = this.pyEnv['mail.guest'].searchRead(
- [['id', '=', member.guest_id[0]]],
- { fields: ['id', 'name'] }
- );
- persona = {
- 'guest': {
- 'id': guest.id,
- 'name': guest.name,
- },
- };
- }
- membersData.push({
- 'id': member.id,
- 'persona': persona,
- });
- }
- return {
- channelMembers: [['insert', membersData]],
- memberCount,
- };
- },
- /**
- * Simulates the `_set_last_seen_message` method of `mail.channel`.
- *
- * @private
- * @param {integer[]} ids
- * @param {integer} message_id
- */
- _mockMailChannel_SetLastSeenMessage(ids, message_id) {
- const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
- this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], {
- fetched_message_id: message_id,
- seen_message_id: message_id,
- });
- },
- /**
- * Simulates the `_link_preview_format` method of `mail.link.preview`.
- *
- * @private
- * @param {object} linkPreview
- */
- _mockMailLinkPreviewFormat(linkPreview) {
- return {
- id: linkPreview.id,
- image_mimetype: linkPreview.image_mimetype,
- og_description: linkPreview.og_description,
- og_image: linkPreview.og_image,
- og_mimetype: linkPreview.og_mimetype,
- og_title: linkPreview.og_title,
- og_type: linkPreview.og_type,
- source_url: linkPreview.source_url
- };
- },
- /**
- * Simulates `mark_all_as_read` on `mail.message`.
- *
- * @private
- * @param {Array[]} [domain]
- * @returns {integer[]}
- */
- _mockMailMessageMarkAllAsRead(domain) {
- const notifDomain = [
- ['res_partner_id', '=', this.currentPartnerId],
- ['is_read', '=', false],
- ];
- if (domain) {
- const messages = this.getRecords('mail.message', domain);
- const ids = messages.map(messages => messages.id);
- this._mockMailMessageSetMessageDone(ids);
- return ids;
- }
- const notifications = this.getRecords('mail.notification', notifDomain);
- this.pyEnv['mail.notification'].write(
- notifications.map(notification => notification.id),
- { is_read: true },
- );
- const messageIds = [];
- for (const notification of notifications) {
- if (!messageIds.includes(notification.mail_message_id)) {
- messageIds.push(notification.mail_message_id);
- }
- }
- const messages = this.getRecords('mail.message', [['id', 'in', messageIds]]);
- // simulate compute that should be done based on notifications
- for (const message of messages) {
- this.pyEnv['mail.message'].write(
- [message.id],
- {
- needaction: false,
- needaction_partner_ids: message.needaction_partner_ids.filter(
- partnerId => partnerId !== this.currentPartnerId
- ),
- },
- );
- }
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.message/mark_as_read', {
- 'message_ids': messageIds,
- 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
- });
- return messageIds;
- },
- /**
- * Simulates `_message_fetch` on `mail.message`.
- *
- * @private
- * @param {Array[]} domain
- * @param {integer} [max_id]
- * @param {integer} [min_id]
- * @param {integer} [limit=30]
- * @returns {Object[]}
- */
- _mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) {
- if (max_id) {
- domain.push(['id', '<', max_id]);
- }
- if (min_id) {
- domain.push(['id', '>', min_id]);
- }
- let messages = this.getRecords('mail.message', domain);
- // sorted from highest ID to lowest ID (i.e. from youngest to oldest)
- messages.sort(function (m1, m2) {
- return m1.id < m2.id ? 1 : -1;
- });
- // pick at most 'limit' messages
- messages.length = Math.min(messages.length, limit);
- return messages;
- },
- /**
- * Simulates `message_format` on `mail.message`.
- *
- * @private
- * @returns {integer[]} ids
- * @returns {Object[]}
- */
- _mockMailMessageMessageFormat(ids) {
- const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
- // sorted from highest ID to lowest ID (i.e. from most to least recent)
- messages.sort(function (m1, m2) {
- return m1.id < m2.id ? 1 : -1;
- });
- return messages.map(message => {
- const thread = message.model && this.getRecords(message.model, [
- ['id', '=', message.res_id],
- ])[0];
- let formattedAuthor;
- if (message.author_id) {
- const [author] = this.getRecords('res.partner', [['id', '=', message.author_id]], { active_test: false });
- formattedAuthor = {
- 'id': author.id,
- 'name': author.name,
- };
- } else {
- formattedAuthor = [['clear']];
- }
- const attachments = this.getRecords('ir.attachment', [
- ['id', 'in', message.attachment_ids],
- ]);
- const formattedAttachments = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
- const allNotifications = this.getRecords('mail.notification', [
- ['mail_message_id', '=', message.id],
- ]);
- const historyPartnerIds = allNotifications
- .filter(notification => notification.is_read)
- .map(notification => notification.res_partner_id);
- const needactionPartnerIds = allNotifications
- .filter(notification => !notification.is_read)
- .map(notification => notification.res_partner_id);
- let notifications = this._mockMailNotification_FilteredForWebClient(
- allNotifications.map(notification => notification.id)
- );
- notifications = this._mockMailNotification_NotificationFormat(
- notifications.map(notification => notification.id)
- );
- const trackingValueIds = this.getRecords('mail.tracking.value', [
- ['id', 'in', message.tracking_value_ids],
- ]);
- const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds);
- const partners = this.getRecords(
- 'res.partner',
- [['id', 'in', message.partner_ids]],
- );
- const linkPreviews = this.getRecords('mail.link.preview', [
- ['id', 'in', message.link_preview_ids],
- ]);
- const linkPreviewsFormatted = linkPreviews.map(linkPreview => this._mockMailLinkPreviewFormat(linkPreview));
-
- const response = Object.assign({}, message, {
- attachment_ids: formattedAttachments,
- author: formattedAuthor,
- history_partner_ids: historyPartnerIds,
- linkPreviews: linkPreviewsFormatted,
- needaction_partner_ids: needactionPartnerIds,
- notifications,
- parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
- recipients: partners.map(p => ({ id: p.id, name: p.name })),
- record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
- trackingValues: formattedTrackingValues,
- });
- delete response['author_id'];
- if (message.subtype_id) {
- const subtype = this.getRecords('mail.message.subtype', [
- ['id', '=', message.subtype_id],
- ])[0];
- response.subtype_description = subtype.description;
- }
- if (message.author_guest_id) {
- const [guest] = this.pyEnv['mail.guest'].searchRead([['id', '=', message.author_guest_id]]);
- response['guestAuthor'] = { id: guest.id, name: guest.name };
- }
- return response;
- });
- },
- /**
- * Simulates `_message_notification_format` on `mail.message`.
- *
- * @private
- * @returns {integer[]} ids
- * @returns {Object[]}
- */
- _mockMailMessage_MessageNotificationFormat(ids) {
- const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
- return messages.map(message => {
- let notifications = this.getRecords('mail.notification', [
- ['mail_message_id', '=', message.id],
- ]);
- notifications = this._mockMailNotification_FilteredForWebClient(
- notifications.map(notification => notification.id)
- );
- notifications = this._mockMailNotification_NotificationFormat(
- notifications.map(notification => notification.id)
- );
- return {
- 'date': message.date,
- 'id': message.id,
- 'message_type': message.message_type,
- 'model': message.model,
- 'notifications': notifications,
- 'res_id': message.res_id,
- 'res_model_name': message.res_model_name,
- };
- });
- },
- /**
- * Simulates `set_message_done` on `mail.message`, which turns provided
- * needaction message to non-needaction (i.e. they are marked as read from
- * from the Inbox mailbox). Also notify on the longpoll bus that the
- * messages have been marked as read, so that UI is updated.
- *
- * @private
- * @param {integer[]} ids
- */
- _mockMailMessageSetMessageDone(ids) {
- const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
-
- const notifications = this.getRecords('mail.notification', [
- ['res_partner_id', '=', this.currentPartnerId],
- ['is_read', '=', false],
- ['mail_message_id', 'in', messages.map(messages => messages.id)]
- ]);
- this.pyEnv['mail.notification'].write(
- notifications.map(notification => notification.id),
- { is_read: true },
- );
- // simulate compute that should be done based on notifications
- for (const message of messages) {
- this.pyEnv['mail.message'].write(
- [message.id],
- {
- needaction: false,
- needaction_partner_ids: message.needaction_partner_ids.filter(
- partnerId => partnerId !== this.currentPartnerId
- ),
- },
- );
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.message/mark_as_read', {
- 'message_ids': [message.id],
- 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
- });
- }
- },
- /**
- * Simulates `toggle_message_starred` on `mail.message`.
- *
- * @private
- * @returns {integer[]} ids
- */
- _mockMailMessageToggleMessageStarred(ids) {
- const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
- for (const message of messages) {
- const wasStared = message.starred_partner_ids.includes(this.currentPartnerId);
- this.pyEnv['mail.message'].write(
- [message.id],
- { starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]] }
- );
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.message/toggle_star', {
- 'message_ids': [message.id],
- 'starred': !wasStared,
- });
- }
- },
- /**
- * Simulates `unstar_all` on `mail.message`.
- *
- * @private
- */
- _mockMailMessageUnstarAll() {
- const messages = this.getRecords('mail.message', [
- ['starred_partner_ids', 'in', this.currentPartnerId],
- ]);
- this.pyEnv['mail.message'].write(
- messages.map(message => message.id),
- { starred_partner_ids: [[3, this.currentPartnerId]] }
- );
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.message/toggle_star', {
- 'message_ids': messages.map(message => message.id),
- 'starred': false,
- });
- },
- /**
- * Simulates `_filtered_for_web_client` on `mail.notification`.
- *
- * @private
- * @returns {integer[]} ids
- * @returns {Object[]}
- */
- _mockMailNotification_FilteredForWebClient(ids) {
- const notifications = this.getRecords('mail.notification', [
- ['id', 'in', ids],
- ]);
- return notifications.filter(notification => {
- const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
- if (['bounce', 'exception', 'canceled'].includes(notification.notification_status) ||
- (partner && partner.partner_share)) {
- return true;
- }
- const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
- const subtypes = (message.subtype_id) ?
- this.getRecords('mail.message.subtype', [['id', '=', message.subtype_id]]) : [];
- return (subtypes.length == 0) || subtypes[0].track_recipients;
- });
- },
- /**
- * Simulates `_notification_format` on `mail.notification`.
- *
- * @private
- * @returns {integer[]} ids
- * @returns {Object[]}
- */
- _mockMailNotification_NotificationFormat(ids) {
- const notifications = this.getRecords('mail.notification', [['id', 'in', ids]]);
- return notifications.map(notification => {
- const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
- return {
- 'id': notification.id,
- 'notification_type': notification.notification_type,
- 'notification_status': notification.notification_status,
- 'failure_type': notification.failure_type,
- 'res_partner_id': partner ? [partner && partner.id, partner && partner.display_name] : undefined,
- };
- });
- },
- /**
- * Simulates `_message_compute_author` on `mail.thread`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @param {Object} [context={}]
- * @returns {Array}
- */
- _MockMailThread_MessageComputeAuthor(model, ids, author_id, email_from, context = {}) {
- if (author_id === undefined) {
- // For simplicity partner is not guessed from email_from here, but
- // that would be the first step on the server.
- let user_id;
- if ('mockedUserId' in context) {
- // can be falsy to simulate not being logged in
- user_id = context.mockedUserId
- ? context.mockedUserId
- : this.publicUserId;
- } else {
- user_id = this.currentUserId;
- }
- const user = this.getRecords(
- 'res.users',
- [['id', '=', user_id]],
- { active_test: false },
- )[0];
- const author = this.getRecords(
- 'res.partner',
- [['id', '=', user.partner_id]],
- { active_test: false },
- )[0];
- author_id = author.id;
- email_from = `${author.display_name} <${author.email}>`;
- }
-
- if (email_from === undefined) {
- if (author_id) {
- const author = this.getRecords(
- 'res.partner',
- [['id', '=', author_id]],
- { active_test: false },
- )[0];
- email_from = `${author.display_name} <${author.email}>`;
- }
- }
-
- if (!email_from) {
- throw Error("Unable to log message due to missing author email.");
- }
-
- return [author_id, email_from];
- },
- /**
- * Simulates `_message_add_suggested_recipient` on `mail.thread`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @param {Object} result
- * @param {Object} [param3={}]
- * @param {string} [param3.email]
- * @param {integer} [param3.partner]
- * @param {string} [param3.reason]
- * @returns {Object}
- */
- _mockMailThread_MessageAddSuggestedRecipient(model, ids, result, { email, partner, reason = '' } = {}) {
- const record = this.getRecords(model, [['id', 'in', 'ids']])[0];
- // for simplicity
- result[record.id].push([partner, email, reason]);
- return result;
- },
- /**
- * Simulates `_message_get_suggested_recipients` on `mail.thread`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockMailThread_MessageGetSuggestedRecipients(model, ids) {
- if (model === 'res.fake') {
- return this._mockResFake_MessageGetSuggestedRecipients(model, ids);
- }
- const result = ids.reduce((result, id) => result[id] = [], {});
- const records = this.getRecords(model, [['id', 'in', ids]]);
- for (const record in records) {
- if (record.user_id) {
- const user = this.getRecords('res.users', [['id', '=', record.user_id]]);
- if (user.partner_id) {
- const reason = this.models[model].fields['user_id'].string;
- this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason);
- }
- }
- }
- return result;
- },
- /**
- * Simulates `_message_get_suggested_recipients` on `res.fake`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockResFake_MessageGetSuggestedRecipients(model, ids) {
- const result = {};
- const records = this.getRecords(model, [['id', 'in', ids]]);
-
- for (const record of records) {
- result[record.id] = [];
- if (record.email_cc) {
- result[record.id].push([
- false,
- record.email_cc,
- undefined,
- 'CC email',
- ]);
- }
- const partners = this.getRecords(
- 'res.partner',
- [['id', 'in', record.partner_ids]],
- );
- if (partners.length) {
- for (const partner of partners) {
- result[record.id].push([
- partner.id,
- partner.display_name,
- undefined,
- 'Email partner',
- ]);
- }
- }
- }
-
- return result;
- },
- /**
- * Simulates `message_post` on `mail.thread`.
- *
- * @private
- * @param {string} model
- * @param {integer[]} ids
- * @param {Object} kwargs
- * @param {Object} [context]
- * @returns {Object}
- */
- _mockMailThreadMessagePost(model, ids, kwargs, context) {
- const id = ids[0]; // ensure_one
- if (kwargs.attachment_ids) {
- const attachments = this.getRecords('ir.attachment', [
- ['id', 'in', kwargs.attachment_ids],
- ['res_model', '=', 'mail.compose.message'],
- ['res_id', '=', 0],
- ]);
- const attachmentIds = attachments.map(attachment => attachment.id);
- this.pyEnv['ir.attachment'].write(
- attachmentIds,
- {
- res_id: id,
- res_model: model,
- },
- );
- kwargs.attachment_ids = attachmentIds.map(attachmentId => [4, attachmentId]);
- }
- const subtype_xmlid = kwargs.subtype_xmlid || 'mail.mt_note';
- const [author_id, email_from] = this._MockMailThread_MessageComputeAuthor(
- model,
- ids,
- kwargs.author_id,
- kwargs.email_from, context,
- );
- const values = Object.assign({}, kwargs, {
- author_id,
- email_from,
- is_discussion: subtype_xmlid === 'mail.mt_comment',
- is_note: subtype_xmlid === 'mail.mt_note',
- model,
- res_id: id,
- });
- delete values.subtype_xmlid;
- const messageId = this.pyEnv['mail.message'].create(values);
- this._mockMailThread_NotifyThread(model, ids, messageId);
- return this._mockMailMessageMessageFormat([messageId])[0];
- },
- /**
- * Simulates `message_subscribe` on `mail.thread`.
- *
- * @private
- * @param {string} model not in server method but necessary for thread mock
- * @param {integer[]} ids
- * @param {integer[]} partner_ids
- * @param {integer[]} subtype_ids
- * @returns {boolean}
- */
- _mockMailThreadMessageSubscribe(model, ids, partner_ids, subtype_ids) {
- for (const id of ids) {
- for (const partner_id of partner_ids) {
- const followerId = this.pyEnv['mail.followers'].create({
- is_active: true,
- partner_id,
- res_id: id,
- res_model: model,
- subtype_ids: subtype_ids,
- });
- this.pyEnv['res.partner'].write([partner_id], {
- message_follower_ids: [followerId],
- });
- }
- }
- },
- /**
- * Simulates `_notify_thread` on `mail.thread`.
- * Simplified version that sends notification to author and channel.
- *
- * @private
- * @param {string} model not in server method but necessary for thread mock
- * @param {integer[]} ids
- * @param {integer} messageId
- * @returns {boolean}
- */
- _mockMailThread_NotifyThread(model, ids, messageId) {
- const message = this.getRecords('mail.message', [['id', '=', messageId]])[0];
- const messageFormat = this._mockMailMessageMessageFormat([messageId])[0];
- const notifications = [];
- if (model === 'mail.channel') {
- // members
- const channels = this.getRecords('mail.channel', [['id', '=', message.res_id]]);
- for (const channel of channels) {
- notifications.push([channel, 'mail.channel/new_message', {
- 'id': channel.id,
- 'message': messageFormat,
- }]);
- // notify update of last_interest_dt
- const now = datetime_to_str(new Date());
- const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
- this.pyEnv['mail.channel.member'].write(
- members.map(member => member.id),
- { last_interest_dt: now },
- );
- for (const member of members) {
- // simplification, send everything on the current user "test" bus, but it should send to each member instead
- notifications.push([member, 'mail.channel/last_interest_dt_changed', {
- 'id': channel.id,
- 'last_interest_dt': member.last_interest_dt,
- }]);
- }
- }
- }
- this.pyEnv['bus.bus']._sendmany(notifications);
- },
- /**
- * Simulates `message_unsubscribe` on `mail.thread`.
- *
- * @private
- * @param {string} model not in server method but necessary for thread mock
- * @param {integer[]} ids
- * @param {integer[]} partner_ids
- * @returns {boolean|undefined}
- */
- _mockMailThreadMessageUnsubscribe(model, ids, partner_ids) {
- if (!partner_ids) {
- return true;
- }
- const followers = this.getRecords('mail.followers', [
- ['res_model', '=', model],
- ['res_id', 'in', ids],
- ['partner_id', 'in', partner_ids || []],
- ]);
- this.pyEnv['mail.followers'].unlink(followers.map(follower => follower.id));
- },
- /**
- * Simulates `_message_track` on `mail.thread`
- */
- _mockMailThread_MessageTrack(modelName, trackedFieldNames, initialTrackedFieldValuesByRecordId) {
- const trackFieldNamesToField = this.mockFieldsGet(modelName, trackedFieldNames);
- const tracking = {};
- const records = this.models[modelName].records;
- for (const record of records) {
- tracking[record.id] = this._mockMailBaseModel_MailTrack(modelName, trackFieldNamesToField, initialTrackedFieldValuesByRecordId[record.id], record);
- }
- for (const record of records) {
- const { trackingValueIds, changedFieldNames } = tracking[record.id] || {};
- if (!changedFieldNames || !changedFieldNames.length) {
- continue;
- }
- const changedFieldsInitialValues = {};
- const initialFieldValues = initialTrackedFieldValuesByRecordId[record.id];
- for (const fname in changedFieldNames) {
- changedFieldsInitialValues[fname] = initialFieldValues[fname];
- }
- const subtype = this._mockMailThread_TrackSubtype(changedFieldsInitialValues);
- this._mockMailThreadMessagePost(modelName, [record.id], {
- subtype_id: subtype.id,
- tracking_value_ids: trackingValueIds,
- });
- }
- return tracking;
- },
- /**
- * Simulates `_track_finalize` on `mail.thread`
- */
- _mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId) {
- this._mockMailThread_MessageTrack(
- model,
- this._mockMailThread_TrackGetFields(model),
- initialTrackedFieldValuesByRecordId,
- );
- },
- /**
- * Simulates `_track_get_fields` on `mail.thread`
- */
- _mockMailThread_TrackGetFields(model) {
- return Object.entries(this.models[model].fields).reduce((prev, next) => {
- if (next[1].tracking) {
- prev.push(next[0]);
- }
- return prev;
- }, []);
- },
- /**
- * Simulates `_track_prepare` on `mail.thread`
- */
- _mockMailThread_TrackPrepare(model) {
- const trackedFieldNames = this._mockMailThread_TrackGetFields(model);
- if (!trackedFieldNames.length) {
- return;
- }
- const initialTrackedFieldValuesByRecordId = {};
- for (const record of this.models[model].records) {
- const values = {};
- initialTrackedFieldValuesByRecordId[record.id] = values;
- for (const fname of trackedFieldNames) {
- values[fname] = record[fname];
- }
- }
- return initialTrackedFieldValuesByRecordId;
- },
- /**
- * Simulates `_track_subtype` on `mail.thread`
- */
- _mockMailThread_TrackSubtype(initialFieldValuesByRecordId) {
- return false;
- },
- /**
- * Simulates `create_tracking_values` on `mail.tracking.value`
- */
- _mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fieldName, field, modelName) {
- let isTracked = true;
- const irField = this.models['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName);
-
- if (!irField) {
- return;
- }
-
- const values = { field: irField['id'], field_desc: field['string'], field_type: field['type'] };
- switch (values.field_type) {
- case 'char':
- case 'datetime':
- case 'float':
- case 'integer':
- case 'monetary':
- case 'text':
- values[`old_value_${values.field_type}`] = initialValue;
- values[`new_value_${values.field_type}`] = newValue;
- break;
- case 'date':
- values['old_value_datetime'] = initialValue;
- values['new_value_datetime'] = newValue;
- break;
- case 'boolean':
- values['old_value_integer'] = initialValue ? 1 : 0;
- values['new_value_integer'] = newValue ? 1 : 0;
- break;
- case 'selection':
- values['old_value_char'] = initialValue;
- values['new_value_char'] = newValue;
- break;
- case 'many2one':
- initialValue = initialValue ? this.pyEnv[field.relation].searchRead([['id', '=', initialValue]])[0] : initialValue;
- newValue = newValue ? this.pyEnv[field.relation].searchRead([['id', '=', newValue]])[0] : newValue;
- values['old_value_integer'] = initialValue ? initialValue.id : 0;
- values['new_value_integer'] = newValue ? newValue.id : 0;
- values['old_value_char'] = initialValue ? initialValue.display_name : '';
- values['new_value_char'] = newValue ? newValue.display_name : '';
- break;
- default:
- isTracked = false;
- }
- if (isTracked) {
- return this.pyEnv['mail.tracking.value'].create(values);
- }
- return false;
- },
- /**
- * Simulates `_tracking_value_format` on `mail.tracking.value`
- */
- _mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) {
- const trackingValues = tracking_value_ids.map(tracking => ({
- changedField: tracking.field_desc,
- id: tracking.id,
- newValue: {
- fieldType: tracking.field_type,
- value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new')
- },
- oldValue: {
- fieldType: tracking.field_type,
- value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old')
- },
- }));
- return trackingValues;
- },
- /**
- * Simulates `_get_display_value` on `mail.tracking.value`
- */
- _mockMailTrackingValue_GetDisplayValue(record, type) {
- switch (record.field_type) {
- case 'float':
- case 'integer':
- case 'monetary':
- case 'text':
- return record[`${type}_value_${record.field_type}`];
- case 'datetime':
- if (record[`${type}_value_datetime`]) {
- const datetime = record[`${type}_value_datetime`];
- return `${datetime}Z`;
- } else {
- return record[`${type}_value_datetime`];
- }
- case 'date':
- if (record[`${type}_value_datetime`]) {
- return record[`${type}_value_datetime`];
- } else {
- return record[`${type}_value_datetime`];
- }
- case 'boolean':
- return !!record[`${type}_value_integer`];
- default:
- return record[`${type}_value_char`];
- }
- },
- /**
- * Simulates `_get_channels_as_member` on `res.partner`.
- *
- * @private
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockResPartner_GetChannelsAsMember(ids) {
- const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
- const channelMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id]]);
- const channels = this.getRecords('mail.channel', [
- ['channel_type', 'in', ['channel', 'group']],
- ['channel_member_ids', 'in', channelMembers.map(member => member.id)],
- ]);
- const directMessagesMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
- const directMessages = this.getRecords('mail.channel', [
- ['channel_type', '=', 'chat'],
- ['channel_member_ids', 'in', directMessagesMembers.map(member => member.id)],
- ]);
- return [
- ...channels,
- ...directMessages,
- ];
- },
- /**
- * Simulates `systray_get_activities` on `res.users`.
- *
- * @private
- */
- _mockResUsersSystrayGetActivities() {
- const activities = this.pyEnv['mail.activity'].searchRead([]);
- const userActivitiesByModelName = {};
- for (const activity of activities) {
- const today = date_to_str(new Date());
- if (today === activity['date_deadline']) {
- activity['states'] = 'today';
- } else if (today > activity['date_deadline']) {
- activity['states'] = 'overdue';
- } else {
- activity['states'] = 'planned';
- }
- }
- for (const activity of activities) {
- const modelName = activity['res_model'];
- if (!userActivitiesByModelName[modelName]) {
- userActivitiesByModelName[modelName] = {
- id: modelName, // for simplicity
- model: modelName,
- name: modelName,
- overdue_count: 0,
- planned_count: 0,
- today_count: 0,
- total_count: 0,
- type: 'activity',
- };
- }
- userActivitiesByModelName[modelName][`${activity['states']}_count`] += 1;
- userActivitiesByModelName[modelName]['total_count'] += 1;
- userActivitiesByModelName[modelName].actions = [{
- icon: 'fa-clock-o',
- name: 'Summary',
- }];
- }
- return Object.values(userActivitiesByModelName);
- },
- /**
- * Simulates `_find_or_create_for_user` on `res.users.settings`.
- *
- * @param {Object} user
- * @returns {Object}
- */
- _mockResUsersSettings_FindOrCreateForUser(user_id) {
- let settings = this.getRecords('res.users.settings', [['user_id', '=', user_id]])[0];
- if (!settings) {
- const settingsId = this.pyEnv['res.users.settings'].create({ user_id: user_id });
- settings = this.getRecords('res.users.settings', [['id', '=', settingsId]])[0];
- }
- return settings;
- },
-
- /**
- * @param {integer} id
- * @param {string[]} [fieldsToFormat]
- * @returns {Object}
- */
- _mockResUsersSettings_ResUsersSettingsFormat(id, fieldsToFormat) {
- const [settings] = this.getRecords('res.users.settings', [['id', '=', id]]);
- const ormAutomaticFields = new Set(['create_date', 'create_uid', 'display_name', 'name', 'write_date', 'write_uid', '__last_update']);
- const filterPredicate = fieldsToFormat ? ([fieldName]) => fieldsToFormat.includes(fieldName) : ([fieldName]) => !ormAutomaticFields.has(fieldName);
- const res = Object.fromEntries(Object.entries(settings).filter(filterPredicate));
- if (Object.prototype.hasOwnProperty.call(res, 'user_id')) {
- res.user_id = { id: settings.user_id };
- }
- if (Object.prototype.hasOwnProperty.call(res, 'volume_settings_ids')) {
- const volumeSettings = this._mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(settings.volume_settings_ids);
- res.volume_settings_ids = [['insert', volumeSettings]];
- }
- return res;
- },
-
- /**
- * Simulates `set_res_users_settings` on `res.users.settings`.
- *
- * @param {integer} id
- * @param {Object} newSettings
- */
- _mockResUsersSettingsSetResUsersSettings(id, newSettings) {
- const oldSettings = this.getRecords('res.users.settings', [['id', '=', id]])[0];
- const changedSettings = {};
- for (const setting in newSettings) {
- if (setting in oldSettings && newSettings[setting] !== oldSettings[setting]) {
- changedSettings[setting] = newSettings[setting];
- }
- }
- this.pyEnv['res.users.settings'].write(
- [id],
- changedSettings,
- );
- const [relatedUser] = this.pyEnv['res.users'].searchRead([['id', '=', oldSettings.user_id]]);
- const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', relatedUser.partner_id]]);
- this.pyEnv['bus.bus']._sendone(relatedPartner, 'res.users.settings/insert', { ...changedSettings, id });
- },
-
- _mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(ids) {
- const volumeSettingsRecords = this.getRecords('res.users.settings.volumes', [['id', 'in', ids]]);
- return volumeSettingsRecords.map(volumeSettingsRecord => {
- const [relatedGuest] = this.getRecords('mail.guest', [['id', '=', volumeSettingsRecord.guest_id]]);
- const [relatedPartner] = this.getRecords('res.partner', [['id', '=', volumeSettingsRecord.partner_id]]);
- return {
- guest_id: relatedGuest ? { id: relatedGuest.id, name: relatedGuest.name } : [['clear']],
- id: volumeSettingsRecord.id,
- partner_id: relatedPartner ? { id: relatedPartner.id, name: relatedPartner.name } : [['clear']],
- volume: volumeSettingsRecord.volume,
- };
- });
- },
-
- /**
- * Simulates `get_mention_suggestions` on `res.partner`.
- *
- * @private
- * @returns {Array[]}
- */
- _mockResPartnerGetMentionSuggestions(args) {
- const search = (args.args[0] || args.kwargs.search || '').toLowerCase();
- const limit = args.args[1] || args.kwargs.limit || 8;
- const channel_id = args.args[2] || args.kwargs.channel_id;
-
- /**
- * Returns the given list of partners after filtering it according to
- * the logic of the Python method `get_mention_suggestions` for the
- * given search term. The result is truncated to the given limit and
- * formatted as expected by the original method.
- *
- * @param {Object[]} partners
- * @param {string} search
- * @param {integer} limit
- * @returns {Object[]}
- */
- const mentionSuggestionsFilter = (partners, search, limit) => {
- const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
- partners
- .filter(partner => {
- if (channel_id) {
- const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partner.id]]);
- if (!member) {
- return false;
- }
- }
- // no search term is considered as return all
- if (!search) {
- return true;
- }
- // otherwise name or email must match search term
- if (partner.name && partner.name.toLowerCase().includes(search)) {
- return true;
- }
- if (partner.email && partner.email.toLowerCase().includes(search)) {
- return true;
- }
- return false;
- })
- .map(partner => partner.id)
- ).values()].map(partnerFormat => {
- if (channel_id) {
- const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerFormat.id]]);
- partnerFormat['persona'] = {
- 'channelMembers': [['insert', this._mockMailChannelMember_MailChannelMemberFormat([member.id])]],
- };
- }
- return partnerFormat;
- });
- // reduce results to max limit
- matchingPartners.length = Math.min(matchingPartners.length, limit);
- return matchingPartners;
- };
-
- // add main suggestions based on users
- const partnersFromUsers = this.getRecords('res.users', [])
- .map(user => this.getRecords('res.partner', [['id', '=', user.partner_id]])[0])
- .filter(partner => partner);
- const mainMatchingPartners = mentionSuggestionsFilter(partnersFromUsers, search, limit);
-
- let extraMatchingPartners = [];
- // if not enough results add extra suggestions based on partners
- const remainingLimit = limit - mainMatchingPartners.length;
- if (mainMatchingPartners.length < limit) {
- const partners = this.getRecords('res.partner', [['id', 'not in', mainMatchingPartners.map(partner => partner.id)]]);
- extraMatchingPartners = mentionSuggestionsFilter(partners, search, remainingLimit);
- }
- return mainMatchingPartners.concat(extraMatchingPartners);
- },
- /**
- * Simulates `_get_needaction_count` on `res.partner`.
- *
- * @private
- * @param {integer} id
- * @returns {integer}
- */
- _mockResPartner_GetNeedactionCount(id) {
- const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
- return this.getRecords('mail.notification', [
- ['res_partner_id', '=', partner.id],
- ['is_read', '=', false],
- ]).length;
- },
- /**
- * Simulates `im_search` on `res.partner`.
- *
- * @private
- * @param {string} [name='']
- * @param {integer} [limit=20]
- * @returns {Object[]}
- */
- _mockResPartnerImSearch(name = '', limit = 20) {
- name = name.toLowerCase(); // simulates ILIKE
- // simulates domain with relational parts (not supported by mock server)
- const matchingPartners = this.getRecords('res.users', [])
- .filter(user => {
- const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
- // user must have a partner
- if (!partner) {
- return false;
- }
- // not current partner
- if (partner.id === this.currentPartnerId) {
- return false;
- }
- // no name is considered as return all
- if (!name) {
- return true;
- }
- if (partner.name && partner.name.toLowerCase().includes(name)) {
- return true;
- }
- return false;
- }).map(user => {
- const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
- return {
- id: partner.id,
- name: partner.name,
- };
- }).sort((a, b) => (a.name === b.name) ? (a.id - b.id) : (a.name > b.name) ? 1 : -1);
- matchingPartners.length = Math.min(matchingPartners.length, limit);
- return [...this._mockResPartnerMailPartnerFormat(matchingPartners.map(partner => partner.id)).values()];
- },
- /**
- * Simulates `mail_partner_format` on `res.partner`.
- *
- * @private
- * @returns {integer[]} ids
- * @returns {Map}
- */
- _mockResPartnerMailPartnerFormat(ids) {
- const partners = this.getRecords(
- 'res.partner',
- [['id', 'in', ids]],
- { active_test: false }
- );
- // Servers is also returning `is_internal_user` but not
- // done here for simplification.
- return new Map(partners.map(partner => {
- const users = this.getRecords('res.users', [['id', 'in', partner.user_ids]]);
- const internalUsers = users.filter(user => !user.share);
- let mainUser;
- if (internalUsers.length > 0) {
- mainUser = internalUsers[0];
- } else if (users.length > 0) {
- mainUser = users[0];
- }
- return [partner.id, {
- "active": partner.active,
- "email": partner.email,
- "id": partner.id,
- "im_status": partner.im_status,
- "name": partner.name,
- "user": mainUser ? {
- 'id': mainUser.id,
- } : [['clear']],
- }];
- }));
- },
- /**
- * Simulates `search_for_channel_invite` on `res.partner`.
- *
- * @private
- * @param {string} [search_term='']
- * @param {integer} [channel_id]
- * @param {integer} [limit=30]
- * @returns {Object[]}
- */
- _mockResPartnerSearchForChannelInvite(search_term, channel_id, limit = 30) {
- search_term = search_term.toLowerCase(); // simulates ILIKE
- // simulates domain with relational parts (not supported by mock server)
- const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
- this.getRecords('res.users', [])
- .filter(user => {
- const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
- // user must have a partner
- if (!partner) {
- return false;
- }
- // not current partner
- if (partner.id === this.currentPartnerId) {
- return false;
- }
- // no name is considered as return all
- if (!search_term) {
- return true;
- }
- if (partner.name && partner.name.toLowerCase().includes(search_term)) {
- return true;
- }
- return false;
- })
- .map(user => user.partner_id)
- ).values()];
- const count = matchingPartners.length;
- matchingPartners.length = Math.min(count, limit);
- return {
- count,
- partners: matchingPartners
- };
- },
- /**
- * Simulates `_message_fetch_failed` on `res.partner`.
- *
- * @private
- * @param {integer} id
- * @returns {Object[]}
- */
- _mockResPartner_MessageFetchFailed(id) {
- const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
- const messages = this.getRecords('mail.message', [
- ['author_id', '=', partner.id],
- ['res_id', '!=', 0],
- ['model', '!=', false],
- ['message_type', '!=', 'user_notification'],
- ]).filter(message => {
- // Purpose is to simulate the following domain on mail.message:
- // ['notification_ids.notification_status', 'in', ['bounce', 'exception']],
- // But it's not supported by getRecords domain to follow a relation.
- const notifications = this.getRecords('mail.notification', [
- ['mail_message_id', '=', message.id],
- ['notification_status', 'in', ['bounce', 'exception']],
- ]);
- return notifications.length > 0;
- });
- return this._mockMailMessage_MessageNotificationFormat(messages.map(message => message.id));
- },
- /**
- * Simulates `_init_messaging` on `res.users`.
- *
- * @private
- * @param {integer[]} ids
- * @returns {Object}
- */
- _mockResUsers_InitMessaging(ids) {
- const user = this.getRecords('res.users', [['id', 'in', ids]])[0];
- const userSettings = this._mockResUsersSettings_FindOrCreateForUser(user.id);
- return {
- channels: this._mockMailChannelChannelInfo(this._mockResPartner_GetChannelsAsMember(user.partner_id).map(channel => channel.id)),
- current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id).get(user.partner_id),
- current_user_id: this.currentUserId,
- current_user_settings: this._mockResUsersSettings_ResUsersSettingsFormat(userSettings.id),
- menu_id: false, // not useful in QUnit tests
- needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(user.partner_id),
- partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId).get(this.partnerRootId),
- shortcodes: this.pyEnv['mail.shortcode'].searchRead([], { fields: ['source', 'substitution'] }),
- starred_counter: this.getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length,
- };
- },
- /**
- * Simulate the `notify_cancel_by_type` on `mail.thread` .
- * Note that this method is overridden by snailmail module but not simulated here.
- */
- _mockMailThreadNotifyCancelByType(model, notificationType) {
- // Query matching notifications
- const notifications = this.getRecords('mail.notification', [
- ['notification_type', '=', notificationType],
- ['notification_status', 'in', ['bounce', 'exception']],
- ]).filter(notification => {
- const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
- return message.model === model && message.author_id === this.currentPartnerId;
- });
- // Update notification status
- this.pyEnv['mail.notification'].write(
- notifications.map(notification => notification.id),
- { notification_status: 'canceled' },
- );
- // Send bus notifications to update status of notifications in the web client
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'mail.message/notification_update', {
- 'elements': this._mockMailMessage_MessageNotificationFormat(
- notifications.map(notification => notification.mail_message_id)
- ),
- });
- },
-});
diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/bus.js b/addons/mail/static/tests/helpers/mock_server/controllers/bus.js
new file mode 100644
index 00000000000..35048d973ee
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/controllers/bus.js
@@ -0,0 +1,68 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/controllers/bus', {
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ if (route === '/mail/chat_post') {
+ const uuid = args.uuid;
+ const message_content = args.message_content;
+ const context = args.context;
+ return this._mockRouteMailChatPost(uuid, message_content, context);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates the `/mail/chat_post` route.
+ *
+ * @private
+ * @param {string} uuid
+ * @param {string} message_content
+ * @param {Object} [context={}]
+ * @returns {Object} one key for list of followers and one for subtypes
+ */
+ async _mockRouteMailChatPost(uuid, message_content, context = {}) {
+ const mailChannel = this.getRecords('mail.channel', [['uuid', '=', uuid]])[0];
+ if (!mailChannel) {
+ return false;
+ }
+
+ let user_id;
+ // find the author from the user session
+ if ('mockedUserId' in context) {
+ // can be falsy to simulate not being logged in
+ user_id = context.mockedUserId;
+ } else {
+ user_id = this.currentUserId;
+ }
+ let author_id;
+ let email_from;
+ if (user_id) {
+ const author = this.getRecords('res.users', [['id', '=', user_id]])[0];
+ author_id = author.partner_id;
+ email_from = `${author.display_name} <${author.email}>`;
+ } else {
+ author_id = false;
+ // simpler fallback than catchall_formatted
+ email_from = mailChannel.anonymous_name || "catchall@example.com";
+ }
+ // supposedly should convert plain text to html
+ const body = message_content;
+ // ideally should be posted with mail_create_nosubscribe=True
+ return this._mockMailChannelMessagePost(
+ mailChannel.id,
+ {
+ author_id,
+ email_from,
+ body,
+ message_type: 'comment',
+ subtype_xmlid: 'mail.mt_comment',
+ },
+ context
+ );
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js
index d88a4ddf0bf..897738a306f 100644
--- a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js
+++ b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js
@@ -4,19 +4,83 @@ import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'mail/controllers/discuss', {
+ /**
+ * @override
+ */
+ async performRPC(route, args) {
+ if (route === '/mail/attachment/upload') {
+ const ufile = args.body.get('ufile');
+ const is_pending = args.body.get('is_pending') === 'true';
+ const model = is_pending ? 'mail.compose.message' : args.body.get('thread_model');
+ const id = is_pending ? 0 : parseInt(args.body.get('thread_id'));
+ const attachmentId = this.mockCreate('ir.attachment', {
+ // datas,
+ mimetype: ufile.type,
+ name: ufile.name,
+ res_id: id,
+ res_model: model,
+ });
+ const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0];
+ return {
+ 'filename': attachment.name,
+ 'id': attachment.id,
+ 'mimetype': attachment.mimetype,
+ 'size': attachment.file_size
+ };
+ }
+ return this._super(...arguments);
+ },
/**
* @override
*/
async _performRPC(route, args) {
+ if (route === '/mail/attachment/delete') {
+ const { attachment_id } = args;
+ return this._mockRouteMailAttachmentRemove(attachment_id);
+ }
+ if (route === '/mail/channel/messages') {
+ const { channel_id, min_id, max_id, limit } = args;
+ return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit);
+ }
if (route === '/mail/channel/notify_typing') {
const id = args.channel_id;
const is_typing = args.is_typing;
const context = args.context;
return this._mockRouteMailChannelNotifyTyping(id, is_typing, context);
}
+ if (new RegExp('/mail/channel/\\d+/partner/\\d+/avatar_128').test(route)) {
+ return;
+ }
if (route === '/mail/channel/ping') {
return;
}
+ if (route === '/mail/history/messages') {
+ const { min_id, max_id, limit } = args;
+ return this._mockRouteMailMessageHistory(min_id, max_id, limit);
+ }
+ if (route === '/mail/init_messaging') {
+ return this._mockRouteMailInitMessaging();
+ }
+ if (route === '/mail/inbox/messages') {
+ const { min_id, max_id, limit } = args;
+ return this._mockRouteMailMessageInbox(min_id, max_id, limit);
+ }
+ if (route === '/mail/link_preview') {
+ return this._mockRouteMailLinkPreview(args.message_id);
+ }
+ if (route === '/mail/load_message_failures') {
+ return this._mockRouteMailLoadMessageFailures();
+ }
+ if (route === '/mail/message/post') {
+ if (args.thread_model === 'mail.channel') {
+ return this._mockMailChannelMessagePost(args.thread_id, args.post_data, args.context);
+ }
+ return this._mockMailThreadMessagePost(args.thread_model, [args.thread_id], args.post_data, args.context);
+ }
+ if (route === '/mail/read_subscription_data') {
+ const follower_id = args.follower_id;
+ return this._mockRouteMailReadSubscriptionData(follower_id);
+ }
if (route === '/mail/rtc/channel/join_call') {
return this._mockRouteMailRtcChannelJoinCall(args.channel_id, args.check_rtc_session_ids);
}
@@ -26,8 +90,58 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
if (route === '/mail/rtc/session/update_and_broadcast') {
return;
}
+ if (route === '/mail/starred/messages') {
+ const { min_id, max_id, limit } = args;
+ return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit);
+ }
+ if (route === '/mail/thread/data') {
+ return this._mockRouteMailThreadData(args.thread_model, args.thread_id, args.request_list);
+ }
+ if (route === '/mail/thread/messages') {
+ const { min_id, max_id, limit, thread_model, thread_id } = args;
+ return this._mockRouteMailThreadFetchMessages(thread_model, thread_id, max_id, min_id, limit);
+ }
return this._super(route, args);
},
+ /**
+ * Simulates the `/mail/init_messaging` route.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _mockRouteMailInitMessaging() {
+ return this._mockResUsers_InitMessaging([this.currentUserId]);
+ },
+ /**
+ * Simulates the `/mail/attachment/delete` route.
+ *
+ * @private
+ * @param {integer} attachment_id
+ */
+ async _mockRouteMailAttachmentRemove(attachment_id) {
+ this.pyEnv['bus.bus']._sendone(this.currentPartnerId, 'ir.attachment/delete', { id: attachment_id });
+ return this.pyEnv['ir.attachment'].unlink([attachment_id]);
+ },
+ /**
+ * Simulates the `/mail/channel/messages` route.
+ *
+ * @private
+ * @param {integer} channel_id
+ * @param {integer} limit
+ * @param {integer} max_id
+ * @param {integer} min_id
+ * @returns {Object} list of messages
+ */
+ async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) {
+ const domain = [
+ ['res_id', '=', channel_id],
+ ['model', '=', 'mail.channel'],
+ ['message_type', '!=', 'user_notification'],
+ ];
+ const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
+ this._mockMailMessageSetMessageDone(messages.map(message => message.id));
+ return this._mockMailMessageMessageFormat(messages.map(message => message.id));
+ },
/**
* Simulates the `/mail/channel/notify_typing` route.
*
@@ -41,6 +155,108 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerId]]);
this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
},
+ /**
+ * Simulates the `/mail/load_message_failures` route.
+ *
+ * @private
+ * @returns {Object[]}
+ */
+ _mockRouteMailLoadMessageFailures() {
+ return this._mockResPartner_MessageFetchFailed(this.currentPartnerId);
+ },
+ /**
+ * Simulates `/mail/link_preview` route.
+ *
+ * @private
+ * @param {integer} message_id
+ * @returns {Object}
+ */
+ _mockRouteMailLinkPreview(message_id) {
+ const linkPreviews = [];
+ const [message] = this.pyEnv['mail.message'].searchRead([['id', '=', message_id]]);
+ if (message.body === 'https://make-link-preview.com') {
+ const linkPreviewId = this.pyEnv['mail.link.preview'].create({
+ og_description: "test description",
+ og_title: 'Article title',
+ og_type: 'article',
+ source_url: 'https://make-link-preview.com',
+ });
+ const [linkPreview] = this.pyEnv['mail.link.preview'].searchRead([['id', '=', linkPreviewId]]);
+ linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview));
+ let target = this.pyEnv.currentPartner;
+ if (message.model === 'mail.channel') {
+ target = this.pyEnv['mail.channel'].search([['id', '=', message.res_id]]);
+ }
+ this.pyEnv['bus.bus']._sendmany([[target, 'mail.link.preview/insert', linkPreviews]]);
+ }
+ },
+ /**
+ * Simulates the `/mail/history/messages` route.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) {
+ const domain = [['needaction', '=', false]];
+ const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
+ return this._mockMailMessageMessageFormat(messages.map(message => message.id));
+ },
+ /**
+ * Simulates the `/mail/inbox/messages` route.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) {
+ const domain = [['needaction', '=', true]];
+ const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
+ return this._mockMailMessageMessageFormat(messages.map(message => message.id));
+ },
+ /**
+ * Simulates the `/mail/starred/messages` route.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) {
+ const domain = [['starred_partner_ids', 'in', [this.currentPartnerId]]];
+ const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
+ return this._mockMailMessageMessageFormat(messages.map(message => message.id));
+ },
+ /**
+ * Simulates the `/mail/read_subscription_data` route.
+ *
+ * @private
+ * @param {integer} follower_id
+ * @returns {Object[]} list of followed subtypes
+ */
+ async _mockRouteMailReadSubscriptionData(follower_id) {
+ const follower = this.getRecords('mail.followers', [['id', '=', follower_id]])[0];
+ const subtypes = this.getRecords('mail.message.subtype', [
+ '&',
+ ['hidden', '=', false],
+ '|',
+ ['res_model', '=', follower.res_model],
+ ['res_model', '=', false],
+ ]);
+ const subtypes_list = subtypes.map(subtype => {
+ const parent = this.getRecords('mail.message.subtype', [
+ ['id', '=', subtype.parent_id],
+ ])[0];
+ return {
+ 'default': subtype.default,
+ 'followed': follower.subtype_ids.includes(subtype.id),
+ 'id': subtype.id,
+ 'internal': subtype.internal,
+ 'name': subtype.name,
+ 'parent_model': parent ? parent.res_model : false,
+ 'res_model': subtype.res_model,
+ 'sequence': subtype.sequence,
+ };
+ });
+ // NOTE: server is also doing a sort here, not reproduced for simplicity
+ return subtypes_list;
+ },
/**
* Simulates the `/mail/rtc/channel/join_call` route.
*
@@ -105,4 +321,66 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
}
this.pyEnv['bus.bus']._sendmany(notifications);
},
+ /**
+ * Simulates the `/mail/thread/data` route.
+ *
+ * @param {string} thread_model
+ * @param {integer} thread_id
+ * @param {string[]} request_list
+ * @returns {Object}
+ */
+ async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
+ const res = {
+ 'hasWriteAccess': true, // mimic user with write access by default
+ 'hasReadAccess': true,
+ };
+ const thread = this.pyEnv[thread_model].searchRead([['id', '=', thread_id]])[0];
+ if (!thread) {
+ res['hasReadAccess'] = false;
+ return res;
+ }
+ if (request_list.includes('activities')) {
+ const activities = this.pyEnv['mail.activity'].searchRead([['id', 'in', thread.activity_ids || []]]);
+ res['activities'] = this._mockMailActivityActivityFormat(activities.map(activity => activity.id));
+ }
+ if (request_list.includes('attachments')) {
+ const attachments = this.pyEnv['ir.attachment'].searchRead(
+ [['res_id', '=', thread.id], ['res_model', '=', thread_model]],
+ ); // order not done for simplicity
+ res['attachments'] = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
+ res['mainAttachment'] = thread.message_main_attachment_id ? { 'id': thread.message_main_attachment_id[0] } : [['clear']];
+ }
+ if (request_list.includes('followers')) {
+ const followers = this.pyEnv['mail.followers'].searchRead([['id', 'in', thread.message_follower_ids || []]]);
+ // search read returns many2one relations as an array [id, display_name].
+ // But the original route does not. Thus, we need to change it now.
+ followers.forEach(follower => follower.partner_id = follower.partner_id[0]);
+ res['followers'] = followers;
+ }
+ if (request_list.includes('suggestedRecipients')) {
+ res['suggestedRecipients'] = this._mockMailThread_MessageGetSuggestedRecipients(thread_model, [thread.id])[thread_id];
+ }
+ return res;
+ },
+ /**
+ * Simulates the `/mail/thread/messages` route.
+ *
+ * @private
+ * @param {string} res_model
+ * @param {integer} res_id
+ * @param {integer} max_id
+ * @param {integer} min_id
+ * @param {integer} limit
+ * @returns {Object[]} list of messages
+ */
+ async _mockRouteMailThreadFetchMessages(res_model, res_id, max_id = false, min_id = false, limit = 30) {
+ const domain = [
+ ['res_id', '=', res_id],
+ ['model', '=', res_model],
+ ['message_type', '!=', 'user_notification'],
+ ];
+ const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
+ this._mockMailMessageSetMessageDone(messages.map(message => message.id));
+ return this._mockMailMessageMessageFormat(messages.map(message => message.id));
+ },
});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js b/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js
new file mode 100644
index 00000000000..55939e85d0f
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js
@@ -0,0 +1,63 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/ir_attachment', {
+ async _performRPC(route, args) {
+ if (args.model === 'ir.attachment' && args.method === 'register_as_main_attachment') {
+ const ids = args.args[0];
+ return this._mockIrAttachmentRegisterAsMainAttachment(ids);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `_attachment_format` on `ir.attachment`.
+ *
+ * @private
+ * @param {integer} ids
+ * @returns {Object}
+ */
+ _mockIrAttachment_attachmentFormat(ids) {
+ const attachments = this.mockRead('ir.attachment', [ids]);
+ return attachments.map(attachment => {
+ const res = {
+ 'checksum': attachment.checksum,
+ 'filename': attachment.name,
+ 'id': attachment.id,
+ 'mimetype': attachment.mimetype,
+ 'name': attachment.name,
+ };
+ res['originThread'] = [['insert', {
+ 'id': attachment.res_id,
+ 'model': attachment.res_model,
+ }]];
+ return res;
+ });
+ },
+ /**
+ * Simulates `register_as_main_attachment` on `ir.attachment`.
+ *
+ * @private
+ * @param {integer} ids
+ * @param {boolean} [force=true]
+ * @returns {boolean} dummy value for mock server
+ */
+ _mockIrAttachmentRegisterAsMainAttachment(ids, force = true) {
+ const [attachment] = this.getRecords('ir.attachment', [['id', 'in', ids]]);
+ if (!attachment.res_model) {
+ return true; // dummy value for mock server
+ }
+ if (!this.models[attachment.res_model].fields['message_main_attachment_id']) {
+ return true; // dummy value for mock server
+ }
+ const [record] = this.pyEnv[attachment.res_model].searchRead([['id', '=', attachment.res_id]]);
+ if (force || !record.message_main_attachment_id) {
+ this.pyEnv[attachment.res_model].write(
+ [record.id],
+ { message_main_attachment_id: attachment.id },
+ );
+ }
+ return true; // dummy value for mock server
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js
new file mode 100644
index 00000000000..5df1f9f92ce
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js
@@ -0,0 +1,165 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/mail_activity', {
+ async _performRPC(route, args) {
+ if (args.model === 'mail.activity' && args.method === 'action_feedback') {
+ const ids = args.args[0];
+ return this._mockMailActivityActionFeedback(ids);
+ }
+ if (args.model === 'mail.activity' && args.method === 'action_feedback_schedule_next') {
+ const ids = args.args[0];
+ return this._mockMailActivityActionFeedbackScheduleNext(ids);
+ }
+ if (args.model === 'mail.activity' && args.method === 'activity_format') {
+ const ids = args.args[0];
+ return this._mockMailActivityActivityFormat(ids);
+ }
+ if (args.model === 'mail.activity' && args.method === 'get_activity_data') {
+ const res_model = args.args[0] || args.kwargs.res_model;
+ const domain = args.args[1] || args.kwargs.domain;
+ return this._mockMailActivityGetActivityData(res_model, domain);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `activity_format` on `mail.activity`.
+ *
+ * @private
+ * @param {number[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailActivityActivityFormat(ids) {
+ let res = this.mockRead('mail.activity', [ids]);
+ res = res.map(record => {
+ if (record.mail_template_ids) {
+ record.mail_template_ids = record.mail_template_ids.map(template_id => {
+ const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
+ return {
+ id: template.id,
+ name: template.name,
+ };
+ });
+ }
+ return record;
+ });
+ return res;
+ },
+ /**
+ * Simulates `_action_done` on `mail.activity`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockMailActivityActionDone(ids) {
+ const activities = this.getRecords('mail.activity', [['id', 'in', ids]]);
+ this.mockUnlink('mail.activity', [activities.map(activity => activity.id)]);
+ },
+ /**
+ * Simulates `action_feedback` on `mail.activity`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockMailActivityActionFeedback(ids) {
+ this._mockMailActivityActionDone(ids);
+ },
+ /**
+ * Simulates `action_feedback_schedule_next` on `mail.activity`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockMailActivityActionFeedbackScheduleNext(ids) {
+ this._mockMailActivityActionDone(ids);
+ return {
+ name: 'Schedule an Activity',
+ view_mode: 'form',
+ res_model: 'mail.activity',
+ views: [[false, 'form']],
+ type: 'ir.actions.act_window',
+ };
+ },
+ /**
+ * Simulates `get_activity_data` on `mail.activity`.
+ *
+ * @private
+ * @param {string} res_model
+ * @param {string} domain
+ * @returns {Object}
+ */
+ _mockMailActivityGetActivityData(res_model, domain) {
+ const self = this;
+ const records = this.getRecords(res_model, domain);
+
+ const activityTypes = this.getRecords('mail.activity.type', []);
+ const activityIds = _.pluck(records, 'activity_ids').flat();
+
+ const groupedActivities = {};
+ const resIdToDeadline = {};
+ const groups = self.mockReadGroup('mail.activity', {
+ domain: [['id', 'in', activityIds]],
+ fields: ['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
+ groupby: ['res_id', 'activity_type_id'],
+ lazy: false,
+ });
+ groups.forEach(function (group) {
+ // mockReadGroup doesn't correctly return all asked fields
+ const activites = self.getRecords('mail.activity', group.__domain);
+ group.activity_type_id = group.activity_type_id[0];
+ let minDate;
+ activites.forEach(function (activity) {
+ if (!minDate || moment(activity.date_deadline) < moment(minDate)) {
+ minDate = activity.date_deadline;
+ }
+ });
+ group.date_deadline = minDate;
+ resIdToDeadline[group.res_id] = minDate;
+ let state;
+ if (group.date_deadline === moment().format("YYYY-MM-DD")) {
+ state = 'today';
+ } else if (moment(group.date_deadline) > moment()) {
+ state = 'planned';
+ } else {
+ state = 'overdue';
+ }
+ if (!groupedActivities[group.res_id]) {
+ groupedActivities[group.res_id] = {};
+ }
+ groupedActivities[group.res_id][group.activity_type_id] = {
+ count: group.__count,
+ state: state,
+ o_closest_deadline: group.date_deadline,
+ ids: _.pluck(activites, 'id'),
+ };
+ });
+
+ return {
+ activity_types: activityTypes.map((type) => {
+ let mailTemplates = [];
+ if (type.mail_template_ids) {
+ mailTemplates = type.mail_template_ids.map((template_id) => {
+ const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
+ return {
+ id: template.id,
+ name: template.name,
+ };
+ });
+ }
+ return [type.id, type.display_name, mailTemplates];
+ }),
+ activity_res_ids: _.sortBy(_.pluck(records, 'id'), function (id) {
+ return moment(resIdToDeadline[id]);
+ }),
+ grouped_activities: groupedActivities,
+ };
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js
new file mode 100644
index 00000000000..740ae76ac81
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js
@@ -0,0 +1,709 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+import { datetime_to_str } from 'web.time';
+
+patch(MockServer.prototype, 'mail/models/mail_channel', {
+ async _performRPC(route, args) {
+ if (args.model === 'mail.channel' && args.method === 'action_unfollow') {
+ const ids = args.args[0];
+ return this._mockMailChannelActionUnfollow(ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_fetched') {
+ const ids = args.args[0];
+ return this._mockMailChannelChannelFetched(ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_fetch_preview') {
+ const ids = args.args[0];
+ return this._mockMailChannelChannelFetchPreview(ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_fold') {
+ const ids = args.args[0];
+ const state = args.args[1] || args.kwargs.state;
+ return this._mockMailChannelChannelFold(ids, state);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_get') {
+ const partners_to = args.args[0] || args.kwargs.partners_to;
+ const pin = args.args[1] !== undefined
+ ? args.args[1]
+ : args.kwargs.pin !== undefined
+ ? args.kwargs.pin
+ : undefined;
+ return this._mockMailChannelChannelGet(partners_to, pin);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_info') {
+ const ids = args.args[0];
+ return this._mockMailChannelChannelInfo(ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'add_members') {
+ const ids = args.args[0];
+ const partner_ids = args.args[1] || args.kwargs.partner_ids;
+ return this._mockMailChannelAddMembers(ids, partner_ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_pin') {
+ const ids = args.args[0];
+ const pinned = args.args[1] || args.kwargs.pinned;
+ return this._mockMailChannelChannelPin(ids, pinned);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_rename') {
+ const ids = args.args[0];
+ const name = args.args[1] || args.kwargs.name;
+ return this._mockMailChannelChannelRename(ids, name);
+ }
+ if (route === '/mail/channel/set_last_seen_message') {
+ const id = args.channel_id;
+ const last_message_id = args.last_message_id;
+ return this._mockMailChannel_ChannelSeen([id], last_message_id);
+ }
+ if (args.model === 'mail.channel' && args.method === 'channel_set_custom_name') {
+ const ids = args.args[0];
+ const name = args.args[1] || args.kwargs.name;
+ return this._mockMailChannelChannelSetCustomName(ids, name);
+ }
+ if (args.model === 'mail.channel' && args.method === 'create_group') {
+ const partners_to = args.args[0] || args.kwargs.partners_to;
+ return this._mockMailChannelCreateGroup(partners_to);
+ }
+ if (args.model === 'mail.channel' && args.method === 'execute_command_leave') {
+ return this._mockMailChannelExecuteCommandLeave(args);
+ }
+ if (args.model === 'mail.channel' && args.method === 'execute_command_who') {
+ return this._mockMailChannelExecuteCommandWho(args);
+ }
+ if (args.model === 'mail.channel' && args.method === 'write' && 'image_128' in args.args[1]) {
+ const ids = args.args[0];
+ return this._mockMailChannelWriteImage128(ids[0]);
+ }
+ if (args.model === 'mail.channel' && args.method === 'load_more_members') {
+ const [channel_ids] = args.args;
+ const { known_member_ids } = args.kwargs;
+ return this._mockMailChannelLoadMoreMembers(channel_ids, known_member_ids);
+ }
+ if (args.model === 'mail.channel' && args.method === 'get_mention_suggestions') {
+ return this._mockMailChannelGetMentionSuggestions(args);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `message_post` on `mail.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @param {Object} kwargs
+ * @param {Object} [context]
+ * @returns {integer|false}
+ */
+ _mockMailChannelMessagePost(id, kwargs, context) {
+ const message_type = kwargs.message_type || 'notification';
+ const channel = this.getRecords('mail.channel', [['id', '=', id]])[0];
+ if (channel.channel_type !== 'channel') {
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
+ this.pyEnv['mail.channel.member'].write(
+ [memberOfCurrentUser.id],
+ {
+ last_interest_dt: datetime_to_str(new Date()),
+ is_pinned: true,
+ },
+ );
+ }
+ const messageData = this._mockMailThreadMessagePost(
+ 'mail.channel',
+ [id],
+ Object.assign(kwargs, {
+ message_type,
+ }),
+ context,
+ );
+ if (kwargs.author_id === this.currentPartnerId) {
+ this._mockMailChannel_SetLastSeenMessage([channel.id], messageData.id);
+ }
+ // simulate compute of message_unread_counter
+ const otherMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '!=', this.currentPartnerId]]);
+ for (const member of otherMembers) {
+ this.pyEnv['mail.channel.member'].write(
+ [member.id],
+ { message_unread_counter: member.message_unread_counter + 1 },
+ );
+ }
+ return messageData;
+ },
+ /**
+ * Simulates `action_unfollow` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockMailChannelActionUnfollow(ids) {
+ const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
+ const [channelMember] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
+ if (!channelMember) {
+ return true;
+ }
+ this.pyEnv['mail.channel'].write(
+ [channel.id],
+ {
+ channel_member_ids: [[2, channelMember.id]],
+ },
+ );
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/leave', {
+ 'id': channel.id,
+ });
+ /**
+ * Leave message not posted here because it would send the new message
+ * notification on a separate bus notification list from the unsubscribe
+ * itself which would lead to the channel being pinned again (handler
+ * for unsubscribe is weak and is relying on both of them to be sent
+ * together on the bus).
+ */
+ // this._mockMailChannelMessagePost(channel.id, {
+ // author_id: this.currentPartnerId,
+ // body: 'left the channel
',
+ // subtype_xmlid: "mail.mt_comment",
+ // });
+ return true;
+ },
+ /**
+ * Simulates `add_members` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @param {integer[]} partner_ids
+ */
+ _mockMailChannelAddMembers(ids, partner_ids) {
+ const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ const partners = this.getRecords('res.partner', [['id', 'in', partner_ids]]);
+ for (const partner of partners) {
+ this.pyEnv['mail.channel.member'].create({
+ channel_id: channel.id,
+ partner_id: partner.id,
+ });
+ const body = `invited ${partner.name} to the channel
`;
+ const message_type = "notification";
+ const subtype_xmlid = "mail.mt_comment";
+ this._mockMailChannelMessagePost(
+ channel.id,
+ { body, message_type, subtype_xmlid },
+ );
+ }
+ this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/joined', {
+ 'channel': this._mockMailChannelChannelInfo([channel.id])[0],
+ 'invited_by_user_id': this.currentUserId,
+ });
+ },
+ /**
+ * Simulates `_broadcast` on `mail.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @param {integer[]} partner_ids
+ * @returns {Object}
+ */
+ _mockMailChannel_broadcast(ids, partner_ids) {
+ const notifications = this._mockMailChannel_channelChannelNotifications(ids, partner_ids);
+ this.pyEnv['bus.bus']._sendmany(notifications);
+ },
+ /**
+ * Simulates `_channel_channel_notifications` on `mail.channel`.
+ *
+ * @private
+ * @param {integer} id
+ * @param {integer[]} partner_ids
+ * @returns {Object}
+ */
+ _mockMailChannel_channelChannelNotifications(ids, partner_ids) {
+ const notifications = [];
+ for (const partner_id of partner_ids) {
+ const user = this.getRecords('res.users', [['partner_id', 'in', partner_id]])[0];
+ if (!user) {
+ continue;
+ }
+ // Note: `channel_info` on the server is supposed to be called with
+ // the proper user context but this is not done here for simplicity.
+ const channelInfos = this._mockMailChannelChannelInfo(ids);
+ const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', partner_id]]);
+ for (const channelInfo of channelInfos) {
+ notifications.push([relatedPartner, 'mail.channel/legacy_insert', channelInfo]);
+ }
+ }
+ return notifications;
+ },
+ /**
+ * Simulates `channel_fetched` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @param {string} extra_info
+ */
+ _mockMailChannelChannelFetched(ids) {
+ const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ for (const channel of channels) {
+ const channelMessages = this.getRecords('mail.message', [
+ ['model', '=', 'mail.channel'],
+ ['res_id', '=', channel.id],
+ ]);
+ const lastMessage = channelMessages.reduce((lastMessage, message) => {
+ if (message.id > lastMessage.id) {
+ return message;
+ }
+ return lastMessage;
+ }, channelMessages[0]);
+ if (!lastMessage) {
+ continue;
+ }
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
+ this.pyEnv['mail.channel.member'].write(
+ [memberOfCurrentUser.id],
+ { fetched_message_id: lastMessage.id },
+ );
+ this.pyEnv['bus.bus']._sendone(channel, 'mail.channel.member/fetched', {
+ 'channel_id': channel.id,
+ 'id': memberOfCurrentUser.id,
+ 'last_message_id': lastMessage.id,
+ 'partner_id': this.currentPartnerId,
+ });
+ }
+ },
+ /**
+ * Simulates `channel_fetch_preview` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @returns {Object[]} list of channels previews
+ */
+ _mockMailChannelChannelFetchPreview(ids) {
+ const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ return channels.map(channel => {
+ const channelMessages = this.getRecords('mail.message', [
+ ['model', '=', 'mail.channel'],
+ ['res_id', '=', channel.id],
+ ]);
+ const lastMessage = channelMessages.reduce((lastMessage, message) => {
+ if (message.id > lastMessage.id) {
+ return message;
+ }
+ return lastMessage;
+ }, channelMessages[0]);
+ return {
+ id: channel.id,
+ last_message: lastMessage ? this._mockMailMessageMessageFormat([lastMessage.id])[0] : false,
+ };
+ });
+ },
+ /**
+ * Simulates the 'channel_fold' route on `mail.channel`.
+ * In particular sends a notification on the bus.
+ *
+ * @private
+ * @param {number} ids
+ * @param {state} [state]
+ */
+ _mockMailChannelChannelFold(ids, state) {
+ const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ for (const channel of channels) {
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
+ const foldState = state ? state : memberOfCurrentUser.fold_state === 'open' ? 'folded' : 'open';
+ const vals = {
+ fold_state: foldState,
+ is_minimized: foldState !== 'closed',
+ };
+ this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], vals);
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.thread/insert', {
+ 'id': channel.id,
+ 'model': 'mail.channel',
+ 'serverFoldState': memberOfCurrentUser.fold_state,
+ });
+ }
+ },
+ /**
+ * Simulates 'channel_get' on 'mail.channel'.
+ *
+ * @private
+ * @param {integer[]} [partners_to=[]]
+ * @param {boolean} [pin=true]
+ * @returns {Object}
+ */
+ _mockMailChannelChannelGet(partners_to = [], pin = true) {
+ if (partners_to.length === 0) {
+ return false;
+ }
+ if (!partners_to.includes(this.currentPartnerId)) {
+ partners_to.push(this.currentPartnerId);
+ }
+ const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
+ // NOTE: this mock is not complete, which is done for simplicity.
+ // Indeed if a chat already exists for the given partners, the server
+ // is supposed to return this existing chat. But the mock is currently
+ // always creating a new chat, because no test is relying on receiving
+ // an existing chat.
+ const id = this.pyEnv['mail.channel'].create({
+ channel_member_ids: partners.map(partner => [0, 0, {
+ partner_id: partner.id,
+ }]),
+ channel_type: 'chat',
+ name: partners.map(partner => partner.name).join(", "),
+ });
+ return this._mockMailChannelChannelInfo([id])[0];
+ },
+ /**
+ * Simulates `channel_info` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailChannelChannelInfo(ids) {
+ const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ return channels.map(channel => {
+ const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
+ const messages = this.getRecords('mail.message', [
+ ['model', '=', 'mail.channel'],
+ ['res_id', '=', channel.id],
+ ]);
+ const [group_public_id] = this.getRecords('res.groups', [
+ ['id', '=', channel.group_public_id],
+ ]);
+ const lastMessageId = messages.reduce((lastMessageId, message) => {
+ if (!lastMessageId || message.id > lastMessageId) {
+ return message.id;
+ }
+ return lastMessageId;
+ }, undefined);
+ const messageNeedactionCounter = this.getRecords('mail.notification', [
+ ['res_partner_id', '=', this.currentPartnerId],
+ ['is_read', '=', false],
+ ['mail_message_id', 'in', messages.map(message => message.id)],
+ ]).length;
+ const channelData = {
+ avatarCacheKey: channel.avatarCacheKey,
+ channel_type: channel.channel_type,
+ id: channel.id,
+ memberCount: channel.member_count,
+ };
+ const res = Object.assign({}, channel, {
+ last_message_id: lastMessageId,
+ message_needaction_counter: messageNeedactionCounter,
+ authorizedGroupFullName: group_public_id ? group_public_id.name : false,
+ });
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
+ if (memberOfCurrentUser) {
+ Object.assign(res, {
+ is_minimized: memberOfCurrentUser.is_minimized,
+ is_pinned: memberOfCurrentUser.is_pinned,
+ last_interest_dt: memberOfCurrentUser.last_interest_dt,
+ message_unread_counter: memberOfCurrentUser.message_unread_counter,
+ state: memberOfCurrentUser.fold_state || 'open',
+ });
+ Object.assign(channelData, {
+ custom_channel_name: memberOfCurrentUser.custom_channel_name,
+ serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter,
+ });
+ if (memberOfCurrentUser.rtc_inviting_session_id) {
+ res['rtc_inviting_session'] = { 'id': memberOfCurrentUser.rtc_inviting_session_id };
+ }
+ channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat([memberOfCurrentUser.id])]];
+ }
+ if (channel.channel_type !== 'channel') {
+ res['seen_partners_info'] = members.filter(member => member.partner_id).map(member => {
+ return {
+ partner_id: member.partner_id,
+ seen_message_id: member.seen_message_id,
+ fetched_message_id: member.fetched_message_id,
+ };
+ });
+ channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat(members.map(member => member.id))]];
+ }
+ res.channel = channelData;
+ return res;
+ });
+ },
+ /**
+ * Simulates the `channel_pin` method of `mail.channel`.
+ *
+ * @private
+ * @param {number[]} ids
+ * @param {boolean} [pinned=false]
+ */
+ async _mockMailChannelChannelPin(ids, pinned = false) {
+ const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId], ['is_pinned', '!=', pinned]]);
+ if (memberOfCurrentUser) {
+ this.pyEnv['mail.channel.member'].write(
+ [memberOfCurrentUser.id],
+ { is_pinned: pinned },
+ );
+ }
+ if (!pinned) {
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/unpin', {
+ 'id': channel.id,
+ });
+ } else {
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/legacy_insert', this._mockMailChannelChannelInfo([channel.id])[0]);
+ }
+ },
+ /**
+ * Simulates the `_channel_seen` method of `mail.channel`.
+ *
+ * @private
+ * @param integer[] ids
+ * @param {integer} last_message_id
+ */
+ async _mockMailChannel_ChannelSeen(ids, last_message_id) {
+ // Update record
+ const channel_id = ids[0];
+ if (!channel_id) {
+ throw new Error('Should only be one channel in channel_seen mock params');
+ }
+ const channel = this.getRecords('mail.channel', [['id', '=', channel_id]])[0];
+ const messagesBeforeGivenLastMessage = this.getRecords('mail.message', [
+ ['id', '<=', last_message_id],
+ ['model', '=', 'mail.channel'],
+ ['res_id', '=', channel.id],
+ ]);
+ if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) {
+ return;
+ }
+ if (!channel) {
+ return;
+ }
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', this.currentPartnerId]]);
+ if (memberOfCurrentUser.seen_message_id && memberOfCurrentUser.seen_message_id >= last_message_id) {
+ return;
+ }
+ this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id);
+ this.pyEnv['bus.bus']._sendone(channel.channel_type === 'chat' ? channel : this.pyEnv.currentPartner, 'mail.channel.member/seen', {
+ 'channel_id': channel.id,
+ 'last_message_id': last_message_id,
+ 'partner_id': this.currentPartnerId,
+ });
+ },
+ /**
+ * Simulates `channel_rename` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockMailChannelChannelRename(ids, name) {
+ const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
+ this.pyEnv['mail.channel'].write(
+ [channel.id],
+ { name },
+ );
+ this.pyEnv['bus.bus']._sendone(channel, 'mail.thread/insert', {
+ 'id': channel.id,
+ 'model': 'mail.channel',
+ 'name': name,
+ });
+ },
+ /**
+ * Simulates `channel_set_custom_name` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockMailChannelChannelSetCustomName(ids, name) {
+ const channelId = ids[0]; // simulate ensure_one.
+ const [memberIdOfCurrentUser] = this.pyEnv['mail.channel.member'].search([['partner_id', '=', this.currentPartnerId], ['channel_id', '=', channelId]]);
+ this.pyEnv['mail.channel.member'].write(
+ [memberIdOfCurrentUser],
+ { custom_channel_name: name },
+ );
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/insert', {
+ 'custom_channel_name': name,
+ 'id': channelId,
+ });
+ },
+ /**
+ * Simulates the `create_group` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} partners_to
+ * @returns {Object}
+ */
+ async _mockMailChannelCreateGroup(partners_to) {
+ const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
+ const id = this.pyEnv['mail.channel'].create({
+ channel_type: 'group',
+ channel_member_ids: partners.map(partner => [0, 0, { partner_id: partner.id }]),
+ name: '',
+ });
+ this._mockMailChannel_broadcast(id, partners.map(partner => partner.id));
+ return this._mockMailChannelChannelInfo([id])[0];
+ },
+ /**
+ * Simulates `execute_command_leave` on `mail.channel`.
+ *
+ * @private
+ */
+ _mockMailChannelExecuteCommandLeave(args) {
+ const channel = this.getRecords('mail.channel', [['id', 'in', args.args[0]]])[0];
+ if (channel.channel_type === 'channel') {
+ this._mockMailChannelActionUnfollow([channel.id]);
+ } else {
+ this._mockMailChannelChannelPin(channel.uuid, false);
+ }
+ },
+ /**
+ * Simulates `execute_command_who` on `mail.channel`.
+ *
+ * @private
+ */
+ _mockMailChannelExecuteCommandWho(args) {
+ const ids = args.args[0];
+ const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
+ for (const channel of channels) {
+ const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
+ const otherPartnerIds = members.filter(member => member.partner_id && member.partner_id !== this.currentPartnerId).map(member => member.partner_id);
+ const otherPartners = this.getRecords('res.partner', [['id', 'in', otherPartnerIds]]);
+ let message = "You are alone in this channel.";
+ if (otherPartners.length > 0) {
+ message = `Users in this channel: ${otherPartners.map(partner => partner.name).join(', ')} and you`;
+ }
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/transient_message', {
+ 'body': `${message}`,
+ 'model': 'mail.channel',
+ 'res_id': channel.id,
+ });
+ }
+ },
+ /**
+ * Simulates `get_mention_suggestions` on `mail.channel`.
+ *
+ * @private
+ * @returns {Array[]}
+ */
+ _mockMailChannelGetMentionSuggestions(args) {
+ const search = args.kwargs.search || '';
+ const limit = args.kwargs.limit || 8;
+
+ /**
+ * Returns the given list of channels after filtering it according to
+ * the logic of the Python method `get_mention_suggestions` for the
+ * given search term. The result is truncated to the given limit and
+ * formatted as expected by the original method.
+ *
+ * @param {Object[]} channels
+ * @param {string} search
+ * @param {integer} limit
+ * @returns {Object[]}
+ */
+ const mentionSuggestionsFilter = function (channels, search, limit) {
+ const matchingChannels = channels
+ .filter(channel => {
+ // no search term is considered as return all
+ if (!search) {
+ return true;
+ }
+ // otherwise name or email must match search term
+ if (channel.name && channel.name.includes(search)) {
+ return true;
+ }
+ return false;
+ }).map(channel => {
+ // expected format
+ return {
+ authorizedGroupFullName: channel.group_public_id ? channel.group_public_id.name : false,
+ channel: {
+ channel_type: channel.channel_type,
+ id: channel.id,
+ },
+ id: channel.id,
+ name: channel.name,
+ };
+ });
+ // reduce results to max limit
+ matchingChannels.length = Math.min(matchingChannels.length, limit);
+ return matchingChannels;
+ };
+
+ const mentionSuggestions = mentionSuggestionsFilter(this.models['mail.channel'].records, search, limit);
+
+ return mentionSuggestions;
+ },
+ /**
+ * Simulates `write` on `mail.channel` when `image_128` changes.
+ *
+ * @param {integer} id
+ */
+ _mockMailChannelWriteImage128(id) {
+ this.pyEnv['mail.channel'].write(
+ [id],
+ {
+ avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"),
+ },
+ );
+ const channel = this.pyEnv['mail.channel'].searchRead([['id', '=', id]])[0];
+ this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/insert', {
+ 'avatarCacheKey': channel.avatarCacheKey,
+ 'id': id,
+ });
+ },
+ /**
+ * Simulates `load_more_members` on `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} channel_ids
+ * @param {integer[]} known_member_ids
+ */
+ _mockMailChannelLoadMoreMembers(channel_ids, known_member_ids) {
+ const members = this.pyEnv['mail.channel.member'].searchRead([
+ ['id', 'not in', known_member_ids],
+ ['channel_id', 'in', channel_ids],
+ ], { limit: 100 });
+ const memberCount = this.pyEnv['mail.channel.member'].searchCount([
+ ['channel_id', 'in', channel_ids],
+ ]);
+ const membersData = [];
+ for (const member of members) {
+ let persona;
+ if (member.partner_id) {
+ const [partner] = this.pyEnv['res.partner'].searchRead(
+ [['id', '=', member.partner_id[0]]],
+ { fields: ['id', 'name', 'im_status'] }
+ );
+ persona = {
+ 'partner': {
+ 'id': partner.id,
+ 'name': partner.name,
+ 'im_status': partner.im_status,
+ },
+ };
+ }
+ if (member.guest_id) {
+ const [guest] = this.pyEnv['mail.guest'].searchRead(
+ [['id', '=', member.guest_id[0]]],
+ { fields: ['id', 'name'] }
+ );
+ persona = {
+ 'guest': {
+ 'id': guest.id,
+ 'name': guest.name,
+ },
+ };
+ }
+ membersData.push({
+ 'id': member.id,
+ 'persona': persona,
+ });
+ }
+ return {
+ channelMembers: [['insert', membersData]],
+ memberCount,
+ };
+ },
+ /**
+ * Simulates the `_set_last_seen_message` method of `mail.channel`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @param {integer} message_id
+ */
+ _mockMailChannel_SetLastSeenMessage(ids, message_id) {
+ const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
+ this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], {
+ fetched_message_id: message_id,
+ seen_message_id: message_id,
+ });
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js b/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js
new file mode 100644
index 00000000000..30b20a18b8d
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js
@@ -0,0 +1,25 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/mail_link_preview', {
+ /**
+ * Simulates the `_link_preview_format` method of `mail.link.preview`.
+ *
+ * @private
+ * @param {object} linkPreview
+ */
+ _mockMailLinkPreviewFormat(linkPreview) {
+ return {
+ id: linkPreview.id,
+ image_mimetype: linkPreview.image_mimetype,
+ og_description: linkPreview.og_description,
+ og_image: linkPreview.og_image,
+ og_mimetype: linkPreview.og_mimetype,
+ og_title: linkPreview.og_title,
+ og_type: linkPreview.og_type,
+ source_url: linkPreview.source_url
+ };
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_message.js b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js
new file mode 100644
index 00000000000..3e0e9102c01
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js
@@ -0,0 +1,294 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/mail_message', {
+ async _performRPC(route, args) {
+ if (args.model === 'mail.message' && args.method === 'mark_all_as_read') {
+ const domain = args.args[0] || args.kwargs.domain;
+ return this._mockMailMessageMarkAllAsRead(domain);
+ }
+ if (args.model === 'mail.message' && args.method === 'message_format') {
+ const ids = args.args[0];
+ return this._mockMailMessageMessageFormat(ids);
+ }
+ if (args.model === 'mail.message' && args.method === 'set_message_done') {
+ const ids = args.args[0];
+ return this._mockMailMessageSetMessageDone(ids);
+ }
+ if (args.model === 'mail.message' && args.method === 'toggle_message_starred') {
+ const ids = args.args[0];
+ return this._mockMailMessageToggleMessageStarred(ids);
+ }
+ if (args.model === 'mail.message' && args.method === 'unstar_all') {
+ return this._mockMailMessageUnstarAll();
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `mark_all_as_read` on `mail.message`.
+ *
+ * @private
+ * @param {Array[]} [domain]
+ * @returns {integer[]}
+ */
+ _mockMailMessageMarkAllAsRead(domain) {
+ const notifDomain = [
+ ['res_partner_id', '=', this.currentPartnerId],
+ ['is_read', '=', false],
+ ];
+ if (domain) {
+ const messages = this.getRecords('mail.message', domain);
+ const ids = messages.map(messages => messages.id);
+ this._mockMailMessageSetMessageDone(ids);
+ return ids;
+ }
+ const notifications = this.getRecords('mail.notification', notifDomain);
+ this.pyEnv['mail.notification'].write(
+ notifications.map(notification => notification.id),
+ { is_read: true },
+ );
+ const messageIds = [];
+ for (const notification of notifications) {
+ if (!messageIds.includes(notification.mail_message_id)) {
+ messageIds.push(notification.mail_message_id);
+ }
+ }
+ const messages = this.getRecords('mail.message', [['id', 'in', messageIds]]);
+ // simulate compute that should be done based on notifications
+ for (const message of messages) {
+ this.pyEnv['mail.message'].write(
+ [message.id],
+ {
+ needaction: false,
+ needaction_partner_ids: message.needaction_partner_ids.filter(
+ partnerId => partnerId !== this.currentPartnerId
+ ),
+ },
+ );
+ }
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', {
+ 'message_ids': messageIds,
+ 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
+ });
+ return messageIds;
+ },
+ /**
+ * Simulates `_message_fetch` on `mail.message`.
+ *
+ * @private
+ * @param {Array[]} domain
+ * @param {integer} [max_id]
+ * @param {integer} [min_id]
+ * @param {integer} [limit=30]
+ * @returns {Object[]}
+ */
+ _mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) {
+ if (max_id) {
+ domain.push(['id', '<', max_id]);
+ }
+ if (min_id) {
+ domain.push(['id', '>', min_id]);
+ }
+ let messages = this.getRecords('mail.message', domain);
+ // sorted from highest ID to lowest ID (i.e. from youngest to oldest)
+ messages.sort(function (m1, m2) {
+ return m1.id < m2.id ? 1 : -1;
+ });
+ // pick at most 'limit' messages
+ messages.length = Math.min(messages.length, limit);
+ return messages;
+ },
+ /**
+ * Simulates `message_format` on `mail.message`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailMessageMessageFormat(ids) {
+ const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
+ // sorted from highest ID to lowest ID (i.e. from most to least recent)
+ messages.sort(function (m1, m2) {
+ return m1.id < m2.id ? 1 : -1;
+ });
+ return messages.map(message => {
+ const thread = message.model && this.getRecords(message.model, [
+ ['id', '=', message.res_id],
+ ])[0];
+ let formattedAuthor;
+ if (message.author_id) {
+ const [author] = this.getRecords('res.partner', [['id', '=', message.author_id]], { active_test: false });
+ formattedAuthor = {
+ 'id': author.id,
+ 'name': author.name,
+ };
+ } else {
+ formattedAuthor = [['clear']];
+ }
+ const attachments = this.getRecords('ir.attachment', [
+ ['id', 'in', message.attachment_ids],
+ ]);
+ const formattedAttachments = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
+ const allNotifications = this.getRecords('mail.notification', [
+ ['mail_message_id', '=', message.id],
+ ]);
+ const historyPartnerIds = allNotifications
+ .filter(notification => notification.is_read)
+ .map(notification => notification.res_partner_id);
+ const needactionPartnerIds = allNotifications
+ .filter(notification => !notification.is_read)
+ .map(notification => notification.res_partner_id);
+ let notifications = this._mockMailNotification_FilteredForWebClient(
+ allNotifications.map(notification => notification.id)
+ );
+ notifications = this._mockMailNotification_NotificationFormat(
+ notifications.map(notification => notification.id)
+ );
+ const trackingValueIds = this.getRecords('mail.tracking.value', [
+ ['id', 'in', message.tracking_value_ids],
+ ]);
+ const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds);
+ const partners = this.getRecords(
+ 'res.partner',
+ [['id', 'in', message.partner_ids]],
+ );
+ const linkPreviews = this.getRecords('mail.link.preview', [
+ ['id', 'in', message.link_preview_ids],
+ ]);
+ const linkPreviewsFormatted = linkPreviews.map(linkPreview => this._mockMailLinkPreviewFormat(linkPreview));
+
+ const response = Object.assign({}, message, {
+ attachment_ids: formattedAttachments,
+ author: formattedAuthor,
+ history_partner_ids: historyPartnerIds,
+ linkPreviews: linkPreviewsFormatted,
+ needaction_partner_ids: needactionPartnerIds,
+ notifications,
+ parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
+ recipients: partners.map(p => ({ id: p.id, name: p.name })),
+ record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
+ trackingValues: formattedTrackingValues,
+ });
+ delete response['author_id'];
+ if (message.subtype_id) {
+ const subtype = this.getRecords('mail.message.subtype', [
+ ['id', '=', message.subtype_id],
+ ])[0];
+ response.subtype_description = subtype.description;
+ }
+ if (message.author_guest_id) {
+ const [guest] = this.pyEnv['mail.guest'].searchRead([['id', '=', message.author_guest_id]]);
+ response['guestAuthor'] = { id: guest.id, name: guest.name };
+ }
+ return response;
+ });
+ },
+ /**
+ * Simulates `_message_notification_format` on `mail.message`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailMessage_MessageNotificationFormat(ids) {
+ const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
+ return messages.map(message => {
+ let notifications = this.getRecords('mail.notification', [
+ ['mail_message_id', '=', message.id],
+ ]);
+ notifications = this._mockMailNotification_FilteredForWebClient(
+ notifications.map(notification => notification.id)
+ );
+ notifications = this._mockMailNotification_NotificationFormat(
+ notifications.map(notification => notification.id)
+ );
+ return {
+ 'date': message.date,
+ 'id': message.id,
+ 'message_type': message.message_type,
+ 'model': message.model,
+ 'notifications': notifications,
+ 'res_id': message.res_id,
+ 'res_model_name': message.res_model_name,
+ };
+ });
+ },
+ /**
+ * Simulates `set_message_done` on `mail.message`, which turns provided
+ * needaction message to non-needaction (i.e. they are marked as read from
+ * from the Inbox mailbox). Also notify on the longpoll bus that the
+ * messages have been marked as read, so that UI is updated.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockMailMessageSetMessageDone(ids) {
+ const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
+
+ const notifications = this.getRecords('mail.notification', [
+ ['res_partner_id', '=', this.currentPartnerId],
+ ['is_read', '=', false],
+ ['mail_message_id', 'in', messages.map(messages => messages.id)]
+ ]);
+ this.pyEnv['mail.notification'].write(
+ notifications.map(notification => notification.id),
+ { is_read: true },
+ );
+ // simulate compute that should be done based on notifications
+ for (const message of messages) {
+ this.pyEnv['mail.message'].write(
+ [message.id],
+ {
+ needaction: false,
+ needaction_partner_ids: message.needaction_partner_ids.filter(
+ partnerId => partnerId !== this.currentPartnerId
+ ),
+ },
+ );
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', {
+ 'message_ids': [message.id],
+ 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
+ });
+ }
+ },
+ /**
+ * Simulates `toggle_message_starred` on `mail.message`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ */
+ _mockMailMessageToggleMessageStarred(ids) {
+ const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
+ for (const message of messages) {
+ const wasStared = message.starred_partner_ids.includes(this.currentPartnerId);
+ this.pyEnv['mail.message'].write(
+ [message.id],
+ { starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]] }
+ );
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', {
+ 'message_ids': [message.id],
+ 'starred': !wasStared,
+ });
+ }
+ },
+ /**
+ * Simulates `unstar_all` on `mail.message`.
+ *
+ * @private
+ */
+ _mockMailMessageUnstarAll() {
+ const messages = this.getRecords('mail.message', [
+ ['starred_partner_ids', 'in', this.currentPartnerId],
+ ]);
+ this.pyEnv['mail.message'].write(
+ messages.map(message => message.id),
+ { starred_partner_ids: [[3, this.currentPartnerId]] }
+ );
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', {
+ 'message_ids': messages.map(message => message.id),
+ 'starred': false,
+ });
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js b/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js
new file mode 100644
index 00000000000..0d56a95cbd2
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js
@@ -0,0 +1,51 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/mail_notification', {
+ /**
+ * Simulates `_filtered_for_web_client` on `mail.notification`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailNotification_FilteredForWebClient(ids) {
+ const notifications = this.getRecords('mail.notification', [
+ ['id', 'in', ids],
+ ]);
+ return notifications.filter(notification => {
+ const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
+ if (['bounce', 'exception', 'canceled'].includes(notification.notification_status) ||
+ (partner && partner.partner_share)) {
+ return true;
+ }
+ const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
+ const subtypes = (message.subtype_id) ?
+ this.getRecords('mail.message.subtype', [['id', '=', message.subtype_id]]) : [];
+ return (subtypes.length == 0) || subtypes[0].track_recipients;
+ });
+ },
+ /**
+ * Simulates `_notification_format` on `mail.notification`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ * @returns {Object[]}
+ */
+ _mockMailNotification_NotificationFormat(ids) {
+ const notifications = this.getRecords('mail.notification', [['id', 'in', ids]]);
+ return notifications.map(notification => {
+ const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
+ return {
+ 'id': notification.id,
+ 'notification_type': notification.notification_type,
+ 'notification_status': notification.notification_status,
+ 'failure_type': notification.failure_type,
+ 'res_partner_id': partner ? [partner && partner.id, partner && partner.display_name] : undefined,
+ };
+ });
+ },
+
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js b/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js
new file mode 100644
index 00000000000..28b2ad03b11
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js
@@ -0,0 +1,362 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+import { datetime_to_str } from 'web.time';
+
+patch(MockServer.prototype, 'mail/models/mail_thread', {
+ async _performRPC(route, args) {
+ if (args.method === 'message_subscribe') {
+ const ids = args.args[0];
+ const partner_ids = args.args[1] || args.kwargs.partner_ids;
+ const subtype_ids = args.args[2] || args.kwargs.subtype_ids;
+ return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, subtype_ids);
+ }
+ if (args.method === 'message_unsubscribe') {
+ const ids = args.args[0];
+ const partner_ids = args.args[1] || args.kwargs.partner_ids;
+ return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids);
+ }
+ if (args.method === 'message_post') {
+ const id = args.args[0];
+ const kwargs = args.kwargs;
+ const context = kwargs.context;
+ delete kwargs.context;
+ return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
+ }
+ if (args.method === 'notify_cancel_by_type') {
+ return this._mockMailThreadNotifyCancelByType(args.model, args.kwargs.notification_type);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `_message_compute_author` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @param {Object} [context={}]
+ * @returns {Array}
+ */
+ _MockMailThread_MessageComputeAuthor(model, ids, author_id, email_from, context = {}) {
+ if (author_id === undefined) {
+ // For simplicity partner is not guessed from email_from here, but
+ // that would be the first step on the server.
+ let user_id;
+ if ('mockedUserId' in context) {
+ // can be falsy to simulate not being logged in
+ user_id = context.mockedUserId
+ ? context.mockedUserId
+ : this.publicUserId;
+ } else {
+ user_id = this.currentUserId;
+ }
+ const user = this.getRecords(
+ 'res.users',
+ [['id', '=', user_id]],
+ { active_test: false },
+ )[0];
+ const author = this.getRecords(
+ 'res.partner',
+ [['id', '=', user.partner_id]],
+ { active_test: false },
+ )[0];
+ author_id = author.id;
+ email_from = `${author.display_name} <${author.email}>`;
+ }
+
+ if (email_from === undefined) {
+ if (author_id) {
+ const author = this.getRecords(
+ 'res.partner',
+ [['id', '=', author_id]],
+ { active_test: false },
+ )[0];
+ email_from = `${author.display_name} <${author.email}>`;
+ }
+ }
+
+ if (!email_from) {
+ throw Error("Unable to log message due to missing author email.");
+ }
+
+ return [author_id, email_from];
+ },
+ /**
+ * Simulates `_message_add_suggested_recipient` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @param {Object} result
+ * @param {Object} [param3={}]
+ * @param {string} [param3.email]
+ * @param {integer} [param3.partner]
+ * @param {string} [param3.reason]
+ * @returns {Object}
+ */
+ _mockMailThread_MessageAddSuggestedRecipient(model, ids, result, { email, partner, reason = '' } = {}) {
+ const record = this.getRecords(model, [['id', 'in', 'ids']])[0];
+ // for simplicity
+ result[record.id].push([partner, email, reason]);
+ return result;
+ },
+ /**
+ * Simulates `_message_get_suggested_recipients` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockMailThread_MessageGetSuggestedRecipients(model, ids) {
+ if (model === 'res.fake') {
+ return this._mockResFake_MessageGetSuggestedRecipients(model, ids);
+ }
+ const result = ids.reduce((result, id) => result[id] = [], {});
+ const records = this.getRecords(model, [['id', 'in', ids]]);
+ for (const record in records) {
+ if (record.user_id) {
+ const user = this.getRecords('res.users', [['id', '=', record.user_id]]);
+ if (user.partner_id) {
+ const reason = this.models[model].fields['user_id'].string;
+ this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason);
+ }
+ }
+ }
+ return result;
+ },
+ /**
+ * Simulates `message_post` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @param {Object} kwargs
+ * @param {Object} [context]
+ * @returns {Object}
+ */
+ _mockMailThreadMessagePost(model, ids, kwargs, context) {
+ const id = ids[0]; // ensure_one
+ if (kwargs.attachment_ids) {
+ const attachments = this.getRecords('ir.attachment', [
+ ['id', 'in', kwargs.attachment_ids],
+ ['res_model', '=', 'mail.compose.message'],
+ ['res_id', '=', 0],
+ ]);
+ const attachmentIds = attachments.map(attachment => attachment.id);
+ this.pyEnv['ir.attachment'].write(
+ attachmentIds,
+ {
+ res_id: id,
+ res_model: model,
+ },
+ );
+ kwargs.attachment_ids = attachmentIds.map(attachmentId => [4, attachmentId]);
+ }
+ const subtype_xmlid = kwargs.subtype_xmlid || 'mail.mt_note';
+ const [author_id, email_from] = this._MockMailThread_MessageComputeAuthor(
+ model,
+ ids,
+ kwargs.author_id,
+ kwargs.email_from, context,
+ );
+ const values = Object.assign({}, kwargs, {
+ author_id,
+ email_from,
+ is_discussion: subtype_xmlid === 'mail.mt_comment',
+ is_note: subtype_xmlid === 'mail.mt_note',
+ model,
+ res_id: id,
+ });
+ delete values.subtype_xmlid;
+ const messageId = this.pyEnv['mail.message'].create(values);
+ this._mockMailThread_NotifyThread(model, ids, messageId);
+ return this._mockMailMessageMessageFormat([messageId])[0];
+ },
+ /**
+ * Simulates `message_subscribe` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model not in server method but necessary for thread mock
+ * @param {integer[]} ids
+ * @param {integer[]} partner_ids
+ * @param {integer[]} subtype_ids
+ * @returns {boolean}
+ */
+ _mockMailThreadMessageSubscribe(model, ids, partner_ids, subtype_ids) {
+ for (const id of ids) {
+ for (const partner_id of partner_ids) {
+ const followerId = this.pyEnv['mail.followers'].create({
+ is_active: true,
+ partner_id,
+ res_id: id,
+ res_model: model,
+ subtype_ids: subtype_ids,
+ });
+ this.pyEnv['res.partner'].write([partner_id], {
+ message_follower_ids: [followerId],
+ });
+ }
+ }
+ },
+ /**
+ * Simulates `_notify_thread` on `mail.thread`.
+ * Simplified version that sends notification to author and channel.
+ *
+ * @private
+ * @param {string} model not in server method but necessary for thread mock
+ * @param {integer[]} ids
+ * @param {integer} messageId
+ * @returns {boolean}
+ */
+ _mockMailThread_NotifyThread(model, ids, messageId) {
+ const message = this.getRecords('mail.message', [['id', '=', messageId]])[0];
+ const messageFormat = this._mockMailMessageMessageFormat([messageId])[0];
+ const notifications = [];
+ if (model === 'mail.channel') {
+ // members
+ const channels = this.getRecords('mail.channel', [['id', '=', message.res_id]]);
+ for (const channel of channels) {
+ notifications.push([channel, 'mail.channel/new_message', {
+ 'id': channel.id,
+ 'message': messageFormat,
+ }]);
+ // notify update of last_interest_dt
+ const now = datetime_to_str(new Date());
+ const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
+ this.pyEnv['mail.channel.member'].write(
+ members.map(member => member.id),
+ { last_interest_dt: now },
+ );
+ for (const member of members) {
+ // simplification, send everything on the current user "test" bus, but it should send to each member instead
+ notifications.push([member, 'mail.channel/last_interest_dt_changed', {
+ 'id': channel.id,
+ 'last_interest_dt': member.last_interest_dt,
+ }]);
+ }
+ }
+ }
+ this.pyEnv['bus.bus']._sendmany(notifications);
+ },
+ /**
+ * Simulates `message_unsubscribe` on `mail.thread`.
+ *
+ * @private
+ * @param {string} model not in server method but necessary for thread mock
+ * @param {integer[]} ids
+ * @param {integer[]} partner_ids
+ * @returns {boolean|undefined}
+ */
+ _mockMailThreadMessageUnsubscribe(model, ids, partner_ids) {
+ if (!partner_ids) {
+ return true;
+ }
+ const followers = this.getRecords('mail.followers', [
+ ['res_model', '=', model],
+ ['res_id', 'in', ids],
+ ['partner_id', 'in', partner_ids || []],
+ ]);
+ this.pyEnv['mail.followers'].unlink(followers.map(follower => follower.id));
+ },
+ /**
+ * Simulates `_message_track` on `mail.thread`
+ */
+ _mockMailThread_MessageTrack(modelName, trackedFieldNames, initialTrackedFieldValuesByRecordId) {
+ const trackFieldNamesToField = this.mockFieldsGet(modelName, trackedFieldNames);
+ const tracking = {};
+ const records = this.models[modelName].records;
+ for (const record of records) {
+ tracking[record.id] = this._mockMailBaseModel__MailTrack(modelName, trackFieldNamesToField, initialTrackedFieldValuesByRecordId[record.id], record);
+ }
+ for (const record of records) {
+ const { trackingValueIds, changedFieldNames } = tracking[record.id] || {};
+ if (!changedFieldNames || !changedFieldNames.length) {
+ continue;
+ }
+ const changedFieldsInitialValues = {};
+ const initialFieldValues = initialTrackedFieldValuesByRecordId[record.id];
+ for (const fname in changedFieldNames) {
+ changedFieldsInitialValues[fname] = initialFieldValues[fname];
+ }
+ const subtype = this._mockMailThread_TrackSubtype(changedFieldsInitialValues);
+ this._mockMailThreadMessagePost(modelName, [record.id], {
+ subtype_id: subtype.id,
+ tracking_value_ids: trackingValueIds,
+ });
+ }
+ return tracking;
+ },
+ /**
+ * Simulates `_track_finalize` on `mail.thread`
+ */
+ _mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId) {
+ this._mockMailThread_MessageTrack(
+ model,
+ this._mockMailThread_TrackGetFields(model),
+ initialTrackedFieldValuesByRecordId,
+ );
+ },
+ /**
+ * Simulates `_track_get_fields` on `mail.thread`
+ */
+ _mockMailThread_TrackGetFields(model) {
+ return Object.entries(this.models[model].fields).reduce((prev, next) => {
+ if (next[1].tracking) {
+ prev.push(next[0]);
+ }
+ return prev;
+ }, []);
+ },
+ /**
+ * Simulates `_track_prepare` on `mail.thread`
+ */
+ _mockMailThread_TrackPrepare(model) {
+ const trackedFieldNames = this._mockMailThread_TrackGetFields(model);
+ if (!trackedFieldNames.length) {
+ return;
+ }
+ const initialTrackedFieldValuesByRecordId = {};
+ for (const record of this.models[model].records) {
+ const values = {};
+ initialTrackedFieldValuesByRecordId[record.id] = values;
+ for (const fname of trackedFieldNames) {
+ values[fname] = record[fname];
+ }
+ }
+ return initialTrackedFieldValuesByRecordId;
+ },
+ /**
+ * Simulates `_track_subtype` on `mail.thread`
+ */
+ _mockMailThread_TrackSubtype(initialFieldValuesByRecordId) {
+ return false;
+ },
+ /**
+ * Simulate the `notify_cancel_by_type` on `mail.thread` .
+ * Note that this method is overridden by snailmail module but not simulated here.
+ */
+ _mockMailThreadNotifyCancelByType(model, notificationType) {
+ // Query matching notifications
+ const notifications = this.getRecords('mail.notification', [
+ ['notification_type', '=', notificationType],
+ ['notification_status', 'in', ['bounce', 'exception']],
+ ]).filter(notification => {
+ const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
+ return message.model === model && message.author_id === this.currentPartnerId;
+ });
+ // Update notification status
+ this.pyEnv['mail.notification'].write(
+ notifications.map(notification => notification.id),
+ { notification_status: 'canceled' },
+ );
+ // Send bus notifications to update status of notifications in the web client
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/notification_update', {
+ 'elements': this._mockMailMessage_MessageNotificationFormat(
+ notifications.map(notification => notification.mail_message_id)
+ ),
+ });
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js b/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js
new file mode 100644
index 00000000000..0ed8cd75b8a
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js
@@ -0,0 +1,133 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/mail_tracking_value', {
+ /**
+ * @override
+ */
+ init(data, options) {
+ this._super(data, options);
+ if (this.currentPartnerId && this.models && 'res.partner' in this.models) {
+ this.currentPartner = this.getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0];
+ }
+ // creation of the ir.model.fields records, required for tracked fields
+ for (const modelName in this.models) {
+ const fieldNamesToFields = this.models[modelName].fields;
+ for (const fname in fieldNamesToFields) {
+ if (fieldNamesToFields[fname].tracking) {
+ this.mockCreate('ir.model.fields', { model: modelName, name: fname });
+ }
+ }
+ }
+ },
+ /**
+ * @override
+ */
+ mockWrite(model) {
+ const initialTrackedFieldValuesByRecordId = this._mockMailThread_TrackPrepare(model);
+ const mockWriteResult = this._super(...arguments);
+ if (initialTrackedFieldValuesByRecordId) {
+ this._mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId);
+ }
+ return mockWriteResult;
+ },
+ /**
+ * Simulates `create_tracking_values` on `mail.tracking.value`
+ */
+ _mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fieldName, field, modelName) {
+ let isTracked = true;
+ const irField = this.models['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName);
+
+ if (!irField) {
+ return;
+ }
+
+ const values = { field: irField['id'], field_desc: field['string'], field_type: field['type'] };
+ switch (values.field_type) {
+ case 'char':
+ case 'datetime':
+ case 'float':
+ case 'integer':
+ case 'monetary':
+ case 'text':
+ values[`old_value_${values.field_type}`] = initialValue;
+ values[`new_value_${values.field_type}`] = newValue;
+ break;
+ case 'date':
+ values['old_value_datetime'] = initialValue;
+ values['new_value_datetime'] = newValue;
+ break;
+ case 'boolean':
+ values['old_value_integer'] = initialValue ? 1 : 0;
+ values['new_value_integer'] = newValue ? 1 : 0;
+ break;
+ case 'selection':
+ values['old_value_char'] = initialValue;
+ values['new_value_char'] = newValue;
+ break;
+ case 'many2one':
+ initialValue = initialValue ? this.pyEnv[field.relation].searchRead([['id', '=', initialValue]])[0] : initialValue;
+ newValue = newValue ? this.pyEnv[field.relation].searchRead([['id', '=', newValue]])[0] : newValue;
+ values['old_value_integer'] = initialValue ? initialValue.id : 0;
+ values['new_value_integer'] = newValue ? newValue.id : 0;
+ values['old_value_char'] = initialValue ? initialValue.display_name : '';
+ values['new_value_char'] = newValue ? newValue.display_name : '';
+ break;
+ default:
+ isTracked = false;
+ }
+ if (isTracked) {
+ return this.pyEnv['mail.tracking.value'].create(values);
+ }
+ return false;
+ },
+ /**
+ * Simulates `_tracking_value_format` on `mail.tracking.value`
+ */
+ _mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) {
+ const trackingValues = tracking_value_ids.map(tracking => ({
+ changedField: tracking.field_desc,
+ id: tracking.id,
+ newValue: {
+ fieldType: tracking.field_type,
+ value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new')
+ },
+ oldValue: {
+ fieldType: tracking.field_type,
+ value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old')
+ },
+ }));
+ return trackingValues;
+ },
+ /**
+ * Simulates `_get_display_value` on `mail.tracking.value`
+ */
+ _mockMailTrackingValue_GetDisplayValue(record, type) {
+ switch (record.field_type) {
+ case 'float':
+ case 'integer':
+ case 'monetary':
+ case 'text':
+ return record[`${type}_value_${record.field_type}`];
+ case 'datetime':
+ if (record[`${type}_value_datetime`]) {
+ const datetime = record[`${type}_value_datetime`];
+ return `${datetime}Z`;
+ } else {
+ return record[`${type}_value_datetime`];
+ }
+ case 'date':
+ if (record[`${type}_value_datetime`]) {
+ return record[`${type}_value_datetime`];
+ } else {
+ return record[`${type}_value_datetime`];
+ }
+ case 'boolean':
+ return !!record[`${type}_value_integer`];
+ default:
+ return record[`${type}_value_char`];
+ }
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/models.js b/addons/mail/static/tests/helpers/mock_server/models/models.js
new file mode 100644
index 00000000000..7127f2c45f7
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/models.js
@@ -0,0 +1,32 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/models', {
+ /**
+ * Simulates `_mail_track` on `base`
+ *
+ * @param {string} model
+ * @param {Object} trackedFieldNamesToField
+ * @param {Object} initialTrackedFieldValues
+ * @param {Object} record
+ * @returns {Object}
+ */
+ _mockMailBaseModel__MailTrack(model, trackedFieldNamesToField, initialTrackedFieldValues, record) {
+ const trackingValueIds = [];
+ const changedFieldNames = [];
+ for (const fname in trackedFieldNamesToField) {
+ const initialValue = initialTrackedFieldValues[fname];
+ const newValue = record[fname];
+ if (initialValue !== newValue) {
+ const tracking = this._mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fname, trackedFieldNamesToField[fname], model);
+ if (tracking) {
+ trackingValueIds.push(tracking);
+ }
+ changedFieldNames.push(fname);
+ }
+ }
+ return { changedFieldNames, trackingValueIds };
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_fake.js b/addons/mail/static/tests/helpers/mock_server/models/res_fake.js
new file mode 100644
index 00000000000..0bf48bf1924
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/res_fake.js
@@ -0,0 +1,46 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/res_fake', {
+ /**
+ * Simulates `_message_get_suggested_recipients` on `res.fake`.
+ *
+ * @private
+ * @param {string} model
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockResFake_MessageGetSuggestedRecipients(model, ids) {
+ const result = {};
+ const records = this.getRecords(model, [['id', 'in', ids]]);
+
+ for (const record of records) {
+ result[record.id] = [];
+ if (record.email_cc) {
+ result[record.id].push([
+ false,
+ record.email_cc,
+ undefined,
+ 'CC email',
+ ]);
+ }
+ const partners = this.getRecords(
+ 'res.partner',
+ [['id', 'in', record.partner_ids]],
+ );
+ if (partners.length) {
+ for (const partner of partners) {
+ result[record.id].push([
+ partner.id,
+ partner.display_name,
+ undefined,
+ 'Email partner',
+ ]);
+ }
+ }
+ }
+ return result;
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_partner.js b/addons/mail/static/tests/helpers/mock_server/models/res_partner.js
new file mode 100644
index 00000000000..b2f8b435ef6
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/res_partner.js
@@ -0,0 +1,282 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/res_partner', {
+ async _performRPC(route, args) {
+ if (args.model === 'res.partner' && args.method === 'im_search') {
+ const name = args.args[0] || args.kwargs.search;
+ const limit = args.args[1] || args.kwargs.limit;
+ return this._mockResPartnerImSearch(name, limit);
+ }
+ if (args.model === 'res.partner' && args.method === 'search_for_channel_invite') {
+ const search_term = args.args[0] || args.kwargs.search_term;
+ const channel_id = args.args[1] || args.kwargs.channel_id;
+ const limit = args.args[2] || args.kwargs.limit;
+ return this._mockResPartnerSearchForChannelInvite(search_term, channel_id, limit);
+ }
+ if (args.model === 'res.partner' && args.method === 'get_mention_suggestions') {
+ return this._mockResPartnerGetMentionSuggestions(args);
+ }
+ return this._super(route, args);
+ },
+
+ /**
+ * Simulates `get_mention_suggestions` on `res.partner`.
+ *
+ * @private
+ * @returns {Array[]}
+ */
+ _mockResPartnerGetMentionSuggestions(args) {
+ const search = (args.args[0] || args.kwargs.search || '').toLowerCase();
+ const limit = args.args[1] || args.kwargs.limit || 8;
+ const channel_id = args.args[2] || args.kwargs.channel_id;
+
+ /**
+ * Returns the given list of partners after filtering it according to
+ * the logic of the Python method `get_mention_suggestions` for the
+ * given search term. The result is truncated to the given limit and
+ * formatted as expected by the original method.
+ *
+ * @param {Object[]} partners
+ * @param {string} search
+ * @param {integer} limit
+ * @returns {Object[]}
+ */
+ const mentionSuggestionsFilter = (partners, search, limit) => {
+ const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
+ partners
+ .filter(partner => {
+ if (channel_id) {
+ const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partner.id]]);
+ if (!member) {
+ return false;
+ }
+ }
+ // no search term is considered as return all
+ if (!search) {
+ return true;
+ }
+ // otherwise name or email must match search term
+ if (partner.name && partner.name.toLowerCase().includes(search)) {
+ return true;
+ }
+ if (partner.email && partner.email.toLowerCase().includes(search)) {
+ return true;
+ }
+ return false;
+ })
+ .map(partner => partner.id)
+ ).values()].map(partnerFormat => {
+ if (channel_id) {
+ const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerFormat.id]]);
+ partnerFormat['persona'] = {
+ 'channelMembers': [['insert', this._mockMailChannelMember_MailChannelMemberFormat([member.id])]],
+ };
+ }
+ return partnerFormat;
+ });
+ // reduce results to max limit
+ matchingPartners.length = Math.min(matchingPartners.length, limit);
+ return matchingPartners;
+ };
+
+ // add main suggestions based on users
+ const partnersFromUsers = this.getRecords('res.users', [])
+ .map(user => this.getRecords('res.partner', [['id', '=', user.partner_id]])[0])
+ .filter(partner => partner);
+ const mainMatchingPartners = mentionSuggestionsFilter(partnersFromUsers, search, limit);
+
+ let extraMatchingPartners = [];
+ // if not enough results add extra suggestions based on partners
+ const remainingLimit = limit - mainMatchingPartners.length;
+ if (mainMatchingPartners.length < limit) {
+ const partners = this.getRecords('res.partner', [['id', 'not in', mainMatchingPartners.map(partner => partner.id)]]);
+ extraMatchingPartners = mentionSuggestionsFilter(partners, search, remainingLimit);
+ }
+ return mainMatchingPartners.concat(extraMatchingPartners);
+ },
+ /**
+ * Simulates `_get_needaction_count` on `res.partner`.
+ *
+ * @private
+ * @param {integer} id
+ * @returns {integer}
+ */
+ _mockResPartner_GetNeedactionCount(id) {
+ const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
+ return this.getRecords('mail.notification', [
+ ['res_partner_id', '=', partner.id],
+ ['is_read', '=', false],
+ ]).length;
+ },
+ /**
+ * Simulates `im_search` on `res.partner`.
+ *
+ * @private
+ * @param {string} [name='']
+ * @param {integer} [limit=20]
+ * @returns {Object[]}
+ */
+ _mockResPartnerImSearch(name = '', limit = 20) {
+ name = name.toLowerCase(); // simulates ILIKE
+ // simulates domain with relational parts (not supported by mock server)
+ const matchingPartners = this.getRecords('res.users', [])
+ .filter(user => {
+ const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
+ // user must have a partner
+ if (!partner) {
+ return false;
+ }
+ // not current partner
+ if (partner.id === this.currentPartnerId) {
+ return false;
+ }
+ // no name is considered as return all
+ if (!name) {
+ return true;
+ }
+ if (partner.name && partner.name.toLowerCase().includes(name)) {
+ return true;
+ }
+ return false;
+ }).map(user => {
+ const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
+ return {
+ id: partner.id,
+ name: partner.name,
+ };
+ }).sort((a, b) => (a.name === b.name) ? (a.id - b.id) : (a.name > b.name) ? 1 : -1);
+ matchingPartners.length = Math.min(matchingPartners.length, limit);
+ return [...this._mockResPartnerMailPartnerFormat(matchingPartners.map(partner => partner.id)).values()];
+ },
+ /**
+ * Simulates `mail_partner_format` on `res.partner`.
+ *
+ * @private
+ * @returns {integer[]} ids
+ * @returns {Map}
+ */
+ _mockResPartnerMailPartnerFormat(ids) {
+ const partners = this.getRecords(
+ 'res.partner',
+ [['id', 'in', ids]],
+ { active_test: false }
+ );
+ // Servers is also returning `is_internal_user` but not
+ // done here for simplification.
+ return new Map(partners.map(partner => {
+ const users = this.getRecords('res.users', [['id', 'in', partner.user_ids]]);
+ const internalUsers = users.filter(user => !user.share);
+ let mainUser;
+ if (internalUsers.length > 0) {
+ mainUser = internalUsers[0];
+ } else if (users.length > 0) {
+ mainUser = users[0];
+ }
+ return [partner.id, {
+ "active": partner.active,
+ "email": partner.email,
+ "id": partner.id,
+ "im_status": partner.im_status,
+ "name": partner.name,
+ "user": mainUser ? {
+ 'id': mainUser.id,
+ } : [['clear']],
+ }];
+ }));
+ },
+ /**
+ * Simulates `search_for_channel_invite` on `res.partner`.
+ *
+ * @private
+ * @param {string} [search_term='']
+ * @param {integer} [channel_id]
+ * @param {integer} [limit=30]
+ * @returns {Object[]}
+ */
+ _mockResPartnerSearchForChannelInvite(search_term, channel_id, limit = 30) {
+ search_term = search_term.toLowerCase(); // simulates ILIKE
+ // simulates domain with relational parts (not supported by mock server)
+ const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
+ this.getRecords('res.users', [])
+ .filter(user => {
+ const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
+ // user must have a partner
+ if (!partner) {
+ return false;
+ }
+ // not current partner
+ if (partner.id === this.currentPartnerId) {
+ return false;
+ }
+ // no name is considered as return all
+ if (!search_term) {
+ return true;
+ }
+ if (partner.name && partner.name.toLowerCase().includes(search_term)) {
+ return true;
+ }
+ return false;
+ })
+ .map(user => user.partner_id)
+ ).values()];
+ const count = matchingPartners.length;
+ matchingPartners.length = Math.min(count, limit);
+ return {
+ count,
+ partners: matchingPartners
+ };
+ },
+ /**
+ * Simulates `_message_fetch_failed` on `res.partner`.
+ *
+ * @private
+ * @param {integer} id
+ * @returns {Object[]}
+ */
+ _mockResPartner_MessageFetchFailed(id) {
+ const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
+ const messages = this.getRecords('mail.message', [
+ ['author_id', '=', partner.id],
+ ['res_id', '!=', 0],
+ ['model', '!=', false],
+ ['message_type', '!=', 'user_notification'],
+ ]).filter(message => {
+ // Purpose is to simulate the following domain on mail.message:
+ // ['notification_ids.notification_status', 'in', ['bounce', 'exception']],
+ // But it's not supported by getRecords domain to follow a relation.
+ const notifications = this.getRecords('mail.notification', [
+ ['mail_message_id', '=', message.id],
+ ['notification_status', 'in', ['bounce', 'exception']],
+ ]);
+ return notifications.length > 0;
+ });
+ return this._mockMailMessage_MessageNotificationFormat(messages.map(message => message.id));
+ },
+ /**
+ * Simulates `_get_channels_as_member` on `res.partner`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockResPartner_GetChannelsAsMember(ids) {
+ const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
+ const channelMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id]]);
+ const channels = this.getRecords('mail.channel', [
+ ['channel_type', 'in', ['channel', 'group']],
+ ['channel_member_ids', 'in', channelMembers.map(member => member.id)],
+ ]);
+ const directMessagesMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
+ const directMessages = this.getRecords('mail.channel', [
+ ['channel_type', '=', 'chat'],
+ ['channel_member_ids', 'in', directMessagesMembers.map(member => member.id)],
+ ]);
+ return [
+ ...channels,
+ ...directMessages,
+ ];
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users.js b/addons/mail/static/tests/helpers/mock_server/models/res_users.js
new file mode 100644
index 00000000000..b9983ca6f46
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/res_users.js
@@ -0,0 +1,78 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+import { date_to_str } from 'web.time';
+
+patch(MockServer.prototype, 'mail/models/res_users', {
+ async _performRPC(route, args) {
+ if (args.model === 'res.users' && args.method === 'systray_get_activities') {
+ return this._mockResUsersSystrayGetActivities();
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `_init_messaging` on `res.users`.
+ *
+ * @private
+ * @param {integer[]} ids
+ * @returns {Object}
+ */
+ _mockResUsers_InitMessaging(ids) {
+ const user = this.getRecords('res.users', [['id', 'in', ids]])[0];
+ const userSettings = this._mockResUsersSettings_FindOrCreateForUser(user.id);
+ return {
+ channels: this._mockMailChannelChannelInfo(this._mockResPartner_GetChannelsAsMember(user.partner_id).map(channel => channel.id)),
+ current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id).get(user.partner_id),
+ current_user_id: this.currentUserId,
+ current_user_settings: this._mockResUsersSettings_ResUsersSettingsFormat(userSettings.id),
+ menu_id: false, // not useful in QUnit tests
+ needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(user.partner_id),
+ partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId).get(this.partnerRootId),
+ shortcodes: this.pyEnv['mail.shortcode'].searchRead([], { fields: ['source', 'substitution'] }),
+ starred_counter: this.getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length,
+ };
+ },
+ /**
+ * Simulates `systray_get_activities` on `res.users`.
+ *
+ * @private
+ */
+ _mockResUsersSystrayGetActivities() {
+ const activities = this.pyEnv['mail.activity'].searchRead([]);
+ const userActivitiesByModelName = {};
+ for (const activity of activities) {
+ const today = date_to_str(new Date());
+ if (today === activity['date_deadline']) {
+ activity['states'] = 'today';
+ } else if (today > activity['date_deadline']) {
+ activity['states'] = 'overdue';
+ } else {
+ activity['states'] = 'planned';
+ }
+ }
+ for (const activity of activities) {
+ const modelName = activity['res_model'];
+ if (!userActivitiesByModelName[modelName]) {
+ userActivitiesByModelName[modelName] = {
+ id: modelName, // for simplicity
+ model: modelName,
+ name: modelName,
+ overdue_count: 0,
+ planned_count: 0,
+ today_count: 0,
+ total_count: 0,
+ type: 'activity',
+ };
+ }
+ userActivitiesByModelName[modelName][`${activity['states']}_count`] += 1;
+ userActivitiesByModelName[modelName]['total_count'] += 1;
+ userActivitiesByModelName[modelName].actions = [{
+ icon: 'fa-clock-o',
+ name: 'Summary',
+ }];
+ }
+ return Object.values(userActivitiesByModelName);
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js
new file mode 100644
index 00000000000..6557d34ffa4
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js
@@ -0,0 +1,78 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/res_users_settings', {
+ async _performRPC(route, args) {
+ if (args.model === 'res.users.settings' && args.method === '_find_or_create_for_user') {
+ const user_id = args.args[0][0];
+ return this._mockResUsersSettings_FindOrCreateForUser(user_id);
+ }
+ if (args.model === 'res.users.settings' && args.method === 'set_res_users_settings') {
+ const id = args.args[0][0];
+ const newSettings = args.kwargs.new_settings;
+ return this._mockResUsersSettingsSetResUsersSettings(id, newSettings);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulates `_find_or_create_for_user` on `res.users.settings`.
+ *
+ * @param {Object} user
+ * @returns {Object}
+ */
+ _mockResUsersSettings_FindOrCreateForUser(user_id) {
+ let settings = this.getRecords('res.users.settings', [['user_id', '=', user_id]])[0];
+ if (!settings) {
+ const settingsId = this.pyEnv['res.users.settings'].create({ user_id: user_id });
+ settings = this.getRecords('res.users.settings', [['id', '=', settingsId]])[0];
+ }
+ return settings;
+ },
+
+ /**
+ * Simulates `res_users_settings_format` on `res.users.settings`.
+ *
+ * @param {integer} id
+ * @param {string[]} [fieldsToFormat]
+ * @returns {Object}
+ */
+ _mockResUsersSettings_ResUsersSettingsFormat(id, fieldsToFormat) {
+ const [settings] = this.getRecords('res.users.settings', [['id', '=', id]]);
+ const ormAutomaticFields = new Set(['create_date', 'create_uid', 'display_name', 'name', 'write_date', 'write_uid', '__last_update']);
+ const filterPredicate = fieldsToFormat ? ([fieldName]) => fieldsToFormat.includes(fieldName) : ([fieldName]) => !ormAutomaticFields.has(fieldName);
+ const res = Object.fromEntries(Object.entries(settings).filter(filterPredicate));
+ if (Object.prototype.hasOwnProperty.call(res, 'user_id')) {
+ res.user_id = { id: settings.user_id };
+ }
+ if (Object.prototype.hasOwnProperty.call(res, 'volume_settings_ids')) {
+ const volumeSettings = this._mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(settings.volume_settings_ids);
+ res.volume_settings_ids = [['insert', volumeSettings]];
+ }
+ return res;
+ },
+
+ /**
+ * Simulates `set_res_users_settings` on `res.users.settings`.
+ *
+ * @param {integer} id
+ * @param {Object} newSettings
+ */
+ _mockResUsersSettingsSetResUsersSettings(id, newSettings) {
+ const oldSettings = this.getRecords('res.users.settings', [['id', '=', id]])[0];
+ const changedSettings = {};
+ for (const setting in newSettings) {
+ if (setting in oldSettings && newSettings[setting] !== oldSettings[setting]) {
+ changedSettings[setting] = newSettings[setting];
+ }
+ }
+ this.pyEnv['res.users.settings'].write(
+ [id],
+ changedSettings,
+ );
+ const [relatedUser] = this.pyEnv['res.users'].searchRead([['id', '=', oldSettings.user_id]]);
+ const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', relatedUser.partner_id]]);
+ this.pyEnv['bus.bus']._sendone(relatedPartner, 'res.users.settings/insert', { ...changedSettings, id });
+ },
+});
diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js
new file mode 100644
index 00000000000..025e9891aee
--- /dev/null
+++ b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js
@@ -0,0 +1,29 @@
+/** @odoo-module **/
+
+// ensure bus mock server is loaded first.
+import '@bus/../tests/helpers/mock_server';
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'mail/models/res_users_settings_volumes', {
+ /**
+ * Simulates `discuss_users_settings_volume_format` on `res.users.settings.volumes`.
+ *
+ * @param {Number[]} ids
+ * @returns {Object}
+ */
+ _mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(ids) {
+ const volumeSettingsRecords = this.getRecords('res.users.settings.volumes', [['id', 'in', ids]]);
+ return volumeSettingsRecords.map(volumeSettingsRecord => {
+ const [relatedGuest] = this.getRecords('mail.guest', [['id', '=', volumeSettingsRecord.guest_id]]);
+ const [relatedPartner] = this.getRecords('res.partner', [['id', '=', volumeSettingsRecord.partner_id]]);
+ return {
+ guest_id: relatedGuest ? { id: relatedGuest.id, name: relatedGuest.name } : [['clear']],
+ id: volumeSettingsRecord.id,
+ partner_id: relatedPartner ? { id: relatedPartner.id, name: relatedPartner.name } : [['clear']],
+ volume: volumeSettingsRecord.volume,
+ };
+ });
+ },
+});
diff --git a/addons/note/static/tests/helpers/mock_server.js b/addons/note/static/tests/helpers/mock_server.js
deleted file mode 100644
index 27b57a30f24..00000000000
--- a/addons/note/static/tests/helpers/mock_server.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/** @odoo-module **/
-
-// ensure mail override is applied first.
-import '@mail/../tests/helpers/mock_server';
-
-import { patch } from '@web/core/utils/patch';
-import { MockServer } from '@web/../tests/helpers/mock_server';
-
-import { date_to_str } from 'web.time';
-
-patch(MockServer.prototype, 'note', {
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- async _performRPC(route, args) {
- if (route === '/note/new') {
- return this._mockRouteNoteNew(args);
- }
- return this._super(...arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Routes
- //--------------------------------------------------------------------------
-
- /**
- * Simulates the `/note/new` route.
- *
- * @private
- */
- _mockRouteNoteNew(values) {
- const noteId = this.pyEnv['note.note'].create({ memo: values['note'] });
- if (values['date_deadline']) {
- this.pyEnv['mail.activity'].create({
- date_deadline: date_to_str(new Date(values['date_deadline'])),
- note_id: noteId,
- res_model: 'note.note',
- });
- }
- },
-
- //--------------------------------------------------------------------------
- // Private Mocked Methods
- //--------------------------------------------------------------------------
-
- /**
- * Simulates `systray_get_activities` on `res.users`.
- *
- * @override
- */
- _mockResUsersSystrayGetActivities() {
- const activities = this._super(...arguments);
- const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]);
- if (noteCount) {
- const noteIndex = activities.findIndex(act => act['model'] === 'note.note');
- if (noteIndex) {
- activities[noteIndex]['name'] = 'Notes';
- } else {
- activities.push({
- id: 'note.note', // for simplicity
- type: 'activity',
- name: 'Notes',
- model: 'note.note',
- planned_count: 0,
- today_count: 0,
- overdue_count: 0,
- total_count: 0,
- });
- }
- }
- return activities;
- },
-});
diff --git a/addons/note/static/tests/helpers/mock_server/controller/note.js b/addons/note/static/tests/helpers/mock_server/controller/note.js
new file mode 100644
index 00000000000..2e92a160a9b
--- /dev/null
+++ b/addons/note/static/tests/helpers/mock_server/controller/note.js
@@ -0,0 +1,42 @@
+/** @odoo-module **/
+
+import { patch } from '@web/core/utils/patch';
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+import { date_to_str } from 'web.time';
+
+patch(MockServer.prototype, 'note/controller/note', {
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ if (route === '/note/new') {
+ return this._mockRouteNoteNew(args);
+ }
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private Mocked Routes
+ //--------------------------------------------------------------------------
+
+ /**
+ * Simulates the `/note/new` route.
+ *
+ * @private
+ */
+ _mockRouteNoteNew(values) {
+ const noteId = this.pyEnv['note.note'].create({ memo: values['note'] });
+ if (values['date_deadline']) {
+ this.pyEnv['mail.activity'].create({
+ date_deadline: date_to_str(new Date(values['date_deadline'])),
+ note_id: noteId,
+ res_model: 'note.note',
+ });
+ }
+ },
+});
diff --git a/addons/note/static/tests/helpers/mock_server/models/res_users.js b/addons/note/static/tests/helpers/mock_server/models/res_users.js
new file mode 100644
index 00000000000..03192e1d044
--- /dev/null
+++ b/addons/note/static/tests/helpers/mock_server/models/res_users.js
@@ -0,0 +1,37 @@
+/** @odoo-module **/
+
+// ensure mail override is applied first.
+import '@mail/../tests/helpers/mock_server/models/res_users';
+
+import { patch } from '@web/core/utils/patch';
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+patch(MockServer.prototype, 'note/models/res_users', {
+ /**
+ * Simulates `systray_get_activities` on `res.users`.
+ *
+ * @override
+ */
+ _mockResUsersSystrayGetActivities() {
+ const activities = this._super(...arguments);
+ const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]);
+ if (noteCount) {
+ const noteIndex = activities.findIndex(act => act['model'] === 'note.note');
+ if (noteIndex) {
+ activities[noteIndex]['name'] = 'Notes';
+ } else {
+ activities.push({
+ id: 'note.note', // for simplicity
+ type: 'activity',
+ name: 'Notes',
+ model: 'note.note',
+ planned_count: 0,
+ today_count: 0,
+ overdue_count: 0,
+ total_count: 0,
+ });
+ }
+ }
+ return activities;
+ },
+});
diff --git a/addons/rating/__manifest__.py b/addons/rating/__manifest__.py
index 58cb0563576..f0612823a06 100644
--- a/addons/rating/__manifest__.py
+++ b/addons/rating/__manifest__.py
@@ -31,7 +31,7 @@ This module allows a customer to give rating.
'rating/static/src/scss/rating_templates.scss',
],
'web.tests_assets': [
- 'rating/static/tests/helpers/*.js',
+ 'rating/static/tests/helpers/**/*.js',
],
},
'license': 'LGPL-3',
diff --git a/addons/rating/static/tests/helpers/mock_server.js b/addons/rating/static/tests/helpers/mock_server/models/mail_message.js
similarity index 82%
rename from addons/rating/static/tests/helpers/mock_server.js
rename to addons/rating/static/tests/helpers/mock_server/models/mail_message.js
index 3c3c7a20209..b7ae96a72c7 100644
--- a/addons/rating/static/tests/helpers/mock_server.js
+++ b/addons/rating/static/tests/helpers/mock_server/models/mail_message.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
-import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
+import '@mail/../tests/helpers/mock_server/models/mail_message'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
-patch(MockServer.prototype, 'rating', {
+patch(MockServer.prototype, 'rating/models/mail_message', {
/**
* @override
*/
diff --git a/addons/snailmail/__manifest__.py b/addons/snailmail/__manifest__.py
index 617d659fc44..c49d4b66988 100644
--- a/addons/snailmail/__manifest__.py
+++ b/addons/snailmail/__manifest__.py
@@ -41,7 +41,7 @@ Allows users to send documents by post
'snailmail/static/src/components/*/*.xml',
],
'web.tests_assets': [
- 'snailmail/static/tests/helpers/*.js',
+ 'snailmail/static/tests/helpers/**/*.js',
],
'web.qunit_suite_tests': [
'snailmail/static/tests/qunit_suite_tests/**/*.js',
diff --git a/addons/snailmail/static/tests/helpers/mock_server.js b/addons/snailmail/static/tests/helpers/mock_server/models/mail_message.js
similarity index 90%
rename from addons/snailmail/static/tests/helpers/mock_server.js
rename to addons/snailmail/static/tests/helpers/mock_server/models/mail_message.js
index e42cfe1746b..346393e4263 100644
--- a/addons/snailmail/static/tests/helpers/mock_server.js
+++ b/addons/snailmail/static/tests/helpers/mock_server/models/mail_message.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
-import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
+import '@mail/../tests/helpers/mock_server/models/mail_message'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
-patch(MockServer.prototype, 'snailmail', {
+patch(MockServer.prototype, 'snailmail/models/mail_message', {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -13,7 +13,7 @@ patch(MockServer.prototype, 'snailmail', {
/**
* @override
*/
- async performRPC(route, args) {
+ async _performRPC(route, args) {
if (args.model === 'mail.message' && args.method === 'cancel_letter') {
const ids = args.args[0];
return this._mockMailMessageCancelLetter(ids);
diff --git a/addons/website_livechat/__manifest__.py b/addons/website_livechat/__manifest__.py
index 129b09e8b81..73ca9afefa1 100644
--- a/addons/website_livechat/__manifest__.py
+++ b/addons/website_livechat/__manifest__.py
@@ -54,7 +54,7 @@ Allow website visitors to chat with the collaborators. This module also brings a
'website_livechat/static/tests/tours/**/*',
],
'web.tests_assets': [
- 'website_livechat/static/tests/helpers/*.js',
+ 'website_livechat/static/tests/helpers/**/*.js',
],
'web.qunit_suite_tests': [
'website_livechat/static/tests/qunit_suite_tests/**/*.js',
diff --git a/addons/website_livechat/static/tests/helpers/mock_server.js b/addons/website_livechat/static/tests/helpers/mock_server.js
deleted file mode 100644
index ba7c058c667..00000000000
--- a/addons/website_livechat/static/tests/helpers/mock_server.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/** @odoo-module **/
-
-import '@im_livechat/../tests/helpers/mock_server'; // ensure mail overrides are applied first
-
-import { patch } from "@web/core/utils/patch";
-import { MockServer } from "@web/../tests/helpers/mock_server";
-
-patch(MockServer.prototype, 'website_livechat', {
- /**
- * @override
- */
- async _performRPC(route, args) {
- if (route === '/web/dataset/call_button') {
- return this._mockCallButton(args);
- }
- return this._super(route, args);
- },
- /**
- * Simulate a 'call_button' operation from a view.
- *
- * @override
- */
- _mockCallButton({ args, kwargs, method, model }) {
- if (model === 'website.visitor' && method === 'action_send_chat_request') {
- return this._mockWebsiteVisitorActionSendChatRequest(args[0]);
- }
- return this._super(...arguments);
- },
- /**
- * Overrides to add visitor information to livechat channels.
- *
- * @override
- */
- _mockMailChannelChannelInfo(ids) {
- const channelInfos = this._super(...arguments);
- for (const channelInfo of channelInfos) {
- const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
- if (channel.channel_type === 'livechat' && channelInfo.livechat_visitor_id) {
- const visitor = this.getRecords('website.visitor', [['id', '=', channelInfo.livechat_visitor_id]])[0];
- const country = this.getRecords('res.country', [['id', '=', visitor.country_id]])[0];
- channelInfo.visitor = {
- country_code: country && country.code,
- country_id: country && country.id,
- display_name: visitor.display_name,
- history: visitor.history, // TODO should be computed
- id: visitor.id,
- is_connected: visitor.is_connected,
- lang_name: visitor.lang_name,
- partner_id: visitor.partner_id,
- website_name: visitor.website_name,
- };
- }
- }
- return channelInfos;
- },
- /**
- * @private
- * @param {integer[]} ids
- */
- _mockWebsiteVisitorActionSendChatRequest(ids) {
- const visitors = this.getRecords('website.visitor', [['id', 'in', ids]]);
- for (const visitor of visitors) {
- const country = visitor.country_id
- ? this.getRecords('res.country', [['id', '=', visitor.country_id]])
- : undefined;
- const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`;
- const membersToAdd = [[0, 0, { partner_id: this.currentPartnerId }]];
- if (visitor.partner_id) {
- membersToAdd.push([0, 0, { partner_id: visitor.partner_id }]);
- } else {
- membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
- }
- const livechatId = this.pyEnv['mail.channel'].create({
- anonymous_name: visitor_name,
- channel_member_ids: membersToAdd,
- channel_type: 'livechat',
- livechat_operator_id: this.currentPartnerId,
- });
- // notify operator
- this.pyEnv['bus.bus']._sendone(this.currentPartner, 'website_livechat.send_chat_request',
- this._mockMailChannelChannelInfo([livechatId])[0]
- );
- }
- },
-});
diff --git a/addons/website_livechat/static/tests/helpers/mock_server/controllers/dataset.js b/addons/website_livechat/static/tests/helpers/mock_server/controllers/dataset.js
new file mode 100644
index 00000000000..f0c6f76442a
--- /dev/null
+++ b/addons/website_livechat/static/tests/helpers/mock_server/controllers/dataset.js
@@ -0,0 +1,27 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'website_livechat/controllers/dataset', {
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ if (route === '/web/dataset/call_button') {
+ return this._mockCallButton(args);
+ }
+ return this._super(route, args);
+ },
+ /**
+ * Simulate a 'call_button' operation from a view.
+ *
+ * @override
+ */
+ _mockCallButton({ args, kwargs, method, model }) {
+ if (model === 'website.visitor' && method === 'action_send_chat_request') {
+ return this._mockWebsiteVisitorActionSendChatRequest(args[0]);
+ }
+ return this._super(...arguments);
+ },
+});
diff --git a/addons/website_livechat/static/tests/helpers/mock_server/models/mail_channel.js b/addons/website_livechat/static/tests/helpers/mock_server/models/mail_channel.js
new file mode 100644
index 00000000000..cbfc54aa8e0
--- /dev/null
+++ b/addons/website_livechat/static/tests/helpers/mock_server/models/mail_channel.js
@@ -0,0 +1,36 @@
+/** @odoo-module **/
+
+import '@im_livechat/../tests/helpers/mock_server/models/mail_channel'; // ensure mail overrides are applied first
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'website_livechat/models/mail_channel', {
+ /**
+ * Overrides to add visitor information to livechat channels.
+ *
+ * @override
+ */
+ _mockMailChannelChannelInfo(ids) {
+ const channelInfos = this._super(...arguments);
+ for (const channelInfo of channelInfos) {
+ const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
+ if (channel.channel_type === 'livechat' && channelInfo.livechat_visitor_id) {
+ const visitor = this.getRecords('website.visitor', [['id', '=', channelInfo.livechat_visitor_id]])[0];
+ const country = this.getRecords('res.country', [['id', '=', visitor.country_id]])[0];
+ channelInfo.visitor = {
+ country_code: country && country.code,
+ country_id: country && country.id,
+ display_name: visitor.display_name,
+ history: visitor.history, // TODO should be computed
+ id: visitor.id,
+ is_connected: visitor.is_connected,
+ lang_name: visitor.lang_name,
+ partner_id: visitor.partner_id,
+ website_name: visitor.website_name,
+ };
+ }
+ }
+ return channelInfos;
+ },
+});
diff --git a/addons/website_livechat/static/tests/helpers/mock_server/models/website_visitor.js b/addons/website_livechat/static/tests/helpers/mock_server/models/website_visitor.js
new file mode 100644
index 00000000000..75b005b1a76
--- /dev/null
+++ b/addons/website_livechat/static/tests/helpers/mock_server/models/website_visitor.js
@@ -0,0 +1,36 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from "@web/../tests/helpers/mock_server";
+
+patch(MockServer.prototype, 'website_livechat/models/website_visitor', {
+ /**
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockWebsiteVisitorActionSendChatRequest(ids) {
+ const visitors = this.getRecords('website.visitor', [['id', 'in', ids]]);
+ for (const visitor of visitors) {
+ const country = visitor.country_id
+ ? this.getRecords('res.country', [['id', '=', visitor.country_id]])
+ : undefined;
+ const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`;
+ const membersToAdd = [[0, 0, { partner_id: this.currentPartnerId }]];
+ if (visitor.partner_id) {
+ membersToAdd.push([0, 0, { partner_id: visitor.partner_id }]);
+ } else {
+ membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
+ }
+ const livechatId = this.pyEnv['mail.channel'].create({
+ anonymous_name: visitor_name,
+ channel_member_ids: membersToAdd,
+ channel_type: 'livechat',
+ livechat_operator_id: this.currentPartnerId,
+ });
+ // notify operator
+ this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'website_livechat.send_chat_request',
+ this._mockMailChannelChannelInfo([livechatId])[0]
+ );
+ }
+ },
+});