505 lines
19 KiB
JavaScript
505 lines
19 KiB
JavaScript
/* @odoo-module */
|
|
|
|
import { patch } from "@web/core/utils/patch";
|
|
import { MockServer } from "@web/../tests/helpers/mock_server";
|
|
|
|
import { parseEmail } from "@mail/js/utils";
|
|
import { serializeDateTime, today } from "@web/core/l10n/dates";
|
|
|
|
patch(MockServer.prototype, {
|
|
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_get_followers") {
|
|
const ids = args.args[0];
|
|
const after = args.args[1] || args.kwargs.after;
|
|
const limit = args.args[2] || args.kwargs.limit;
|
|
return this._mockMailThreadMessageGetFollowers(args.model, ids, after, limit);
|
|
}
|
|
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 super._performRPC(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.
|
|
const author = this.getRecords(
|
|
"res.partner",
|
|
[["id", "=", this.pyEnv.currentUser.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];
|
|
},
|
|
/**
|
|
* @param {string} model
|
|
* @param {integer[]} ids
|
|
*
|
|
* @returns {Map<integer:string>}
|
|
* Simulate `_message_compute_subject` on `mail.thread`
|
|
*/
|
|
mockMailThread_MessageComputeSubject(model, ids) {
|
|
const records = this.getRecords(model, [["id", "in", ids]]);
|
|
return new Map(records.map((record) => [record.id, record.name || ""]));
|
|
},
|
|
/**
|
|
* Simulates `_get_customer_information` on `mail.thread`.
|
|
*
|
|
* @private
|
|
* @param {string} model
|
|
* @param {integer[]} ids
|
|
* @returns {Object}
|
|
*/
|
|
_mockMailThread_GetCustomerInformation(model, ids) {
|
|
return {};
|
|
},
|
|
/**
|
|
* 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, lang, reason = "" } = {}
|
|
) {
|
|
const record = this.getRecords(model, [["id", "in", ids]])[0];
|
|
if (email !== undefined && partner === undefined) {
|
|
const partnerInfo = parseEmail(email);
|
|
partner = this.getRecords("res.partner", [["email", "=", partnerInfo[1]]])[0];
|
|
}
|
|
if (partner) {
|
|
result[record.id].push([partner.id, partner.display_name, lang, reason, {}]);
|
|
} else {
|
|
const partnerCreateValues = this._mockMailThread_GetCustomerInformation(model, ids);
|
|
result[record.id].push([false, email, reason, lang, partnerCreateValues]);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
/**
|
|
* Simulates `message_get_followers` on `mail.thread`.
|
|
*
|
|
* @private
|
|
* @param {string} model
|
|
* @param {integer[]} ids
|
|
* @param {integer} [after]
|
|
* @param {integer} [limit=100]
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailThreadMessageGetFollowers(model, ids, after, limit = 100, kwargs = {}) {
|
|
const domain = [
|
|
["res_id", "=", ids[0]],
|
|
["res_model", "=", model],
|
|
];
|
|
if (after) {
|
|
domain.push(["id", ">", after]);
|
|
}
|
|
if (kwargs.filter_recipients) {
|
|
domain.push(["partner_id", "!=", this.pyEnv.currentPartnerId]);
|
|
}
|
|
const followers = this.getRecords("mail.followers", domain).sort(
|
|
(f1, f2) => (f1.id < f2.id ? -1 : 1) // sorted from lowest ID to highest ID (i.e. from oldest to youngest)
|
|
);
|
|
followers.length = Math.min(followers.length, limit);
|
|
return this._mockMailFollowers_FormatForChatter(followers.map((follower) => follower.id));
|
|
},
|
|
/**
|
|
* 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;
|
|
const partner = this.getRecords("res.partner", [["id", "=", user.partner_id]]);
|
|
this._mockMailThread_MessageAddSuggestedRecipient(model, ids, result, {
|
|
email: partner.email,
|
|
partner,
|
|
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.partner_emails) {
|
|
kwargs.partner_ids = kwargs.partner_ids || [];
|
|
for (const email of kwargs.partner_emails) {
|
|
const partner = this.getRecords("res.partner", [["email", "=", email]]);
|
|
if (partner.length !== 0) {
|
|
kwargs.partner_ids.push(partner[0].id);
|
|
} else {
|
|
const partner_id = this.pyEnv["res.partner"].create(
|
|
Object.assign({ email }, kwargs.partner_additional_values[email] || {})
|
|
);
|
|
kwargs.partner_ids.push(partner_id);
|
|
}
|
|
}
|
|
}
|
|
delete kwargs.partner_emails;
|
|
delete kwargs.partner_additional_values;
|
|
if (context?.["mail_post_autofollow"] && kwargs["partner_ids"].length > 0) {
|
|
this._mockMailThreadMessageSubscribe(model, ids, kwargs["partner_ids"]);
|
|
}
|
|
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";
|
|
let author_id;
|
|
let email_from;
|
|
const author_guest_id = this.pyEnv.currentUser?._is_public()
|
|
? this._mockMailGuest__getGuestFromContext()?.id
|
|
: undefined;
|
|
if (!author_guest_id) {
|
|
[author_id, email_from] = this._MockMailThread_MessageComputeAuthor(
|
|
model,
|
|
ids,
|
|
kwargs.author_id,
|
|
kwargs.email_from,
|
|
context
|
|
);
|
|
}
|
|
|
|
const values = Object.assign({}, kwargs, {
|
|
author_id,
|
|
author_guest_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);
|
|
for (const partnerId of kwargs.partner_ids || []) {
|
|
this.pyEnv["mail.notification"].create({
|
|
mail_message_id: messageId,
|
|
notification_type: "inbox",
|
|
res_partner_id: partnerId,
|
|
});
|
|
}
|
|
this._mockMailThread_NotifyThread(model, ids, messageId, context?.temporary_id);
|
|
return Object.assign(this._mockMailMessageMessageFormat([messageId])[0], {
|
|
temporary_id: context?.temporary_id,
|
|
});
|
|
},
|
|
/**
|
|
* 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) {
|
|
let followerId = this.pyEnv["mail.followers"].search([
|
|
["partner_id", "=", partner_id],
|
|
])[0];
|
|
if (!followerId) {
|
|
if (!subtype_ids || subtype_ids.length === 0) {
|
|
subtype_ids = this.pyEnv["mail.message.subtype"].search([
|
|
["default", "=", true],
|
|
"|",
|
|
["res_model", "=", model],
|
|
["res_model", "=", false],
|
|
]);
|
|
}
|
|
followerId = this.pyEnv["mail.followers"].create({
|
|
is_active: true,
|
|
partner_id,
|
|
res_id: id,
|
|
res_model: model,
|
|
subtype_ids: subtype_ids,
|
|
});
|
|
}
|
|
this.pyEnv[model].write(ids, {
|
|
message_follower_ids: [[4, followerId]],
|
|
});
|
|
this.pyEnv["res.partner"].write([partner_id], {
|
|
message_follower_ids: [[4, 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, temporary_id) {
|
|
const message = this.getRecords("mail.message", [["id", "=", messageId]])[0];
|
|
const messageFormat = this._mockMailMessageMessageFormat([messageId])[0];
|
|
const notifications = [];
|
|
if (model === "discuss.channel") {
|
|
const channels = this.getRecords("discuss.channel", [["id", "=", message.res_id]]);
|
|
for (const channel of channels) {
|
|
const now = serializeDateTime(today());
|
|
notifications.push([
|
|
[channel, "members"],
|
|
"mail.record/insert",
|
|
{
|
|
Thread: {
|
|
id: channel.id,
|
|
is_pinned: true,
|
|
model: "discuss.channel",
|
|
},
|
|
},
|
|
]);
|
|
notifications.push([
|
|
channel,
|
|
"discuss.channel/last_interest_dt_changed",
|
|
{
|
|
id: channel.id,
|
|
last_interest_dt: now,
|
|
},
|
|
]);
|
|
notifications.push([
|
|
channel,
|
|
"discuss.channel/new_message",
|
|
{
|
|
id: channel.id,
|
|
message: Object.assign(messageFormat, { temporary_id }),
|
|
},
|
|
]);
|
|
if (message.author_id === this.pyEnv.currentPartnerId) {
|
|
this._mockDiscussChannel_ChannelSeen(ids, message.id);
|
|
}
|
|
}
|
|
}
|
|
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.pyEnv.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)
|
|
),
|
|
}
|
|
);
|
|
},
|
|
});
|