diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index 348bd2699cf..fdc2a66b1ee 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -81,6 +81,8 @@ class ResUsers(models.Model): values.pop('login', None) values.pop('name', None) partner_user.write(values) + if not partner_user.login_date: + partner_user._notify_inviter() return (self.env.cr.dbname, partner_user.login, values.get('password')) else: # user does not exist: sign up invited user @@ -92,7 +94,8 @@ class ResUsers(models.Model): if partner.company_id: values['company_id'] = partner.company_id.id values['company_ids'] = [(6, 0, [partner.company_id.id])] - self._signup_create_user(values) + partner_user = self._signup_create_user(values) + partner_user._notify_inviter() else: # no token, sign up an external user values['email'] = values.get('email') or values.get('login') @@ -110,6 +113,20 @@ class ResUsers(models.Model): raise SignupError(_('Signup is not allowed for uninvited users')) return self._create_user_from_template(values) + @api.multi + def _notify_inviter(self): + for user in self: + invite_partner = user.create_uid.partner_id + if invite_partner: + # notify invite user that new user is connected + title = _("%s connected") % user.name + message = _("This is his first connection. Wish him welcome") + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', invite_partner.id), + {'type': 'user_connection', 'title': title, + 'message': message, 'partner_id': user.partner_id.id} + ) + def _create_user_from_template(self, values): template_user_id = literal_eval(self.env['ir.config_parameter'].sudo().get_param('base.template_portal_user_id', 'False')) template_user = self.browse(template_user_id) diff --git a/addons/bus/static/src/js/services/bus_service.js b/addons/bus/static/src/js/services/bus_service.js index 8e367b7f93a..72ba8c1531b 100644 --- a/addons/bus/static/src/js/services/bus_service.js +++ b/addons/bus/static/src/js/services/bus_service.js @@ -27,11 +27,12 @@ var BusService = CrossTab.extend(ServicesMixin, { * * @param {string} title * @param {string} content + * @param {function} [callback] if given callback will be called when user clicks on notification */ - sendNotification: function (title, content) { + sendNotification: function (title, content, callback) { if (window.Notification && Notification.permission === "granted") { if (this.isMasterTab()) { - this._sendNativeNotification(title, content); + this._sendNativeNotification(title, content, callback); } } else { this.do_notify(title, content); @@ -76,9 +77,10 @@ var BusService = CrossTab.extend(ServicesMixin, { * @private * @param {string} title * @param {string} content + * @param {function} [callback] if given callback will be called when user clicks on notification */ - _sendNativeNotification: function (title, content) { - var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"}); + _sendNativeNotification: function (title, content, callback) { + var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoobot.png"}); notification.onclick = function () { window.focus(); if (this.cancel) { @@ -86,6 +88,9 @@ var BusService = CrossTab.extend(ServicesMixin, { } else if (this.close) { this.close(); } + if (callback) { + callback(); + } }; }, diff --git a/addons/im_livechat_mail_bot/__init__.py b/addons/im_livechat_mail_bot/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/im_livechat_mail_bot/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/im_livechat_mail_bot/__manifest__.py b/addons/im_livechat_mail_bot/__manifest__.py new file mode 100644 index 00000000000..7c1906b78ea --- /dev/null +++ b/addons/im_livechat_mail_bot/__manifest__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Odoobot for livechat', + 'version': '1.0', + 'category': 'Discuss', + 'summary': 'Add livechat support for odoobot', + 'description': "", + 'website': 'https://www.odoo.com/page/discuss', + 'depends': ['mail_bot', 'im_livechat'], + 'installable': True, + 'application': False, + 'auto_install': True, +} diff --git a/addons/im_livechat_mail_bot/models/__init__.py b/addons/im_livechat_mail_bot/models/__init__.py new file mode 100644 index 00000000000..ce6a8455179 --- /dev/null +++ b/addons/im_livechat_mail_bot/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import mail_bot diff --git a/addons/im_livechat_mail_bot/models/mail_bot.py b/addons/im_livechat_mail_bot/models/mail_bot.py new file mode 100644 index 00000000000..57eacc5db5d --- /dev/null +++ b/addons/im_livechat_mail_bot/models/mail_bot.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, _ + + +class MailBot(models.AbstractModel): + _inherit = 'mail.bot' + + def _get_answer(self, record, body, values, command): + if self._is_bot_in_private_channel(record): + if self._is_bot_pinged(values): + return _("That's me! 🎉
Try to type \":\" to use canned responses.") + elif values.get("canned_response_ids"): + discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id + discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"' + chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"' + return _("Good, you can customize your canned responses in the live chat application.

") + \ + _("There are 3 different ways in Odoo to interact with your colleagues:
\ +-via this chat window
\ +-via the Discuss application:


\ +-or via the chatter:


\ +Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src) + + return super(MailBot, self)._get_answer(record, body, values, command) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index a7f4f896b9b..ffe37f5c59a 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -828,6 +828,9 @@ class Channel(models.Model): WHERE C.uuid = %s""", (uuid,)) return self._cr.dictfetchall() + def _channel_fetch_listeners_where_clause(self, uuid): + return ("C.uuid = %s", (uuid,)) + @api.multi def channel_fetch_preview(self): """ Return the last message of the given channels """ @@ -888,7 +891,8 @@ class Channel(models.Model): if self.public == 'private': msg += _(" This channel is private. People must be invited to join it.") else: - channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) + all_channel_partners = self.env['mail.channel.partner'].with_context(active_test=False) + channel_partners = all_channel_partners.search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) msg = _("You are in a private conversation with @%s.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous')) msg += _("""

Type @username to mention someone, and grab his attention.
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 1288f780d49..f086a3220a7 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2030,7 +2030,11 @@ class MailThread(models.AbstractModel): values.pop(x, None) # Post the message + # canned_response_ids are added by js to be used by other computations (odoobot) + # we need to pop it from values since it is not stored on mail.message + canned_response_ids = values.pop('canned_response_ids', False) new_message = MailMessage.create(values) + values['canned_response_ids'] = canned_response_ids self._message_post_after_hook(new_message, values, model_description=model_description, mail_auto_delete=mail_auto_delete) return new_message diff --git a/addons/mail/static/src/img/odoobot.png b/addons/mail/static/src/img/odoobot.png new file mode 100644 index 00000000000..b1921736d32 Binary files /dev/null and b/addons/mail/static/src/img/odoobot.png differ diff --git a/addons/mail/static/src/img/smiley/odoobot.png b/addons/mail/static/src/img/smiley/odoobot.png new file mode 100644 index 00000000000..49513b98ac1 Binary files /dev/null and b/addons/mail/static/src/img/smiley/odoobot.png differ diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 6d402829563..bba52c3f8b2 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -361,6 +361,7 @@ var BasicComposer = Widget.extend({ content: this._mentionManager.generateLinks(value), attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), partner_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('@'), 'id')), + canned_response_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelections()[':'], 'id')), command: commands.length > 0 ? commands[0].name : undefined, }); }, diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index 9bf7a79d1b9..0c6480f3264 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -97,7 +97,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin */ getAvatarSource: function () { if (this._isOdoobotAuthor()) { - return '/mail/static/src/img/odoo_o.png'; + return '/mail/static/src/img/odoobot.png'; } else if (this.hasAuthor()) { return '/web/image/res.partner/' + this.getAuthorID() + '/image_small'; } else if (this.getType() === 'email') { diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js index f1831cd3aa8..5fcd0285a82 100644 --- a/addons/mail/static/src/js/models/threads/thread.js +++ b/addons/mail/static/src/js/models/threads/thread.js @@ -329,6 +329,7 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { partner_ids: data.partner_ids, body: body, attachment_ids: data.attachment_ids, + canned_response_ids: data.canned_response_ids, }; if ('subject' in data) { messageData.subject = data.subject; diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index a5eae053a82..23d19caf31f 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -46,6 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers var MailManager = AbstractService.extend({ dependencies: ['ajax', 'bus_service', 'local_storage'], _ODOOBOT_ID: "ODOOBOT", // default authorID for transient messages + IS_STATIC_PREVIEW_ENABLED: true, /** * @override @@ -254,17 +255,23 @@ var MailManager = AbstractService.extend({ */ getSystrayPreviews: function (filter) { var self = this; + var defs = []; + var channelDef = this._getSystrayChannelPreviews(filter); var inboxDef = this._getSystrayInboxPreviews(filter); var failureDef = this._getSystrayMailFailurePreviews(filter); - - return $.when(channelDef, inboxDef, failureDef) - .then(function (previewsChannel, previewsInbox, previewsFailure) { + defs = defs.concat([channelDef, inboxDef, failureDef]); + if (this.IS_STATIC_PREVIEW_ENABLED) { + var staticDef = this._getSystrayStaticPreviews(filter); + defs.push(staticDef); + } + return $.when.apply($, defs) + .then(function (previewsChannel, previewsInbox, previewsFailure, previewsStatic) { // order: failures > inbox > channel, each group must be sorted previewsChannel = self._sortPreviews(previewsChannel); previewsInbox = self._sortPreviews(previewsInbox); previewsFailure = self._sortPreviews(previewsFailure); - return _.union(previewsFailure, previewsInbox, previewsChannel); + return _.union(previewsFailure, previewsInbox, previewsChannel, previewsStatic); }); }, /** @@ -797,6 +804,28 @@ var MailManager = AbstractService.extend({ return $.when([]); } }, + /** + * Get the previews of static systray elements, adding notification request + * + * @private + * @param {string|undefined} [filter] + */ + _getSystrayStaticPreviews: function (filter) { + var elems = []; + if ( + window.Notification && window.Notification.permission === "default" && + (filter === 'mailbox_inbox' || !filter) + ) { + elems.push({ + title: _t("Odoobot has a request for you"), + imageSRC: "/mail/static/src/img/smiley/odoobot.png", + status: 'bot', + body: _t("Enable desktop notifications to stay up to date"), + id: 'request_notification', + }); + } + return $.when(elems); + }, /** * Initialize the canned responses from the server data * diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index b3e5239f9ee..475cbd44f90 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -165,13 +165,13 @@ MailManager.include({ * Add or remove failure when receiving a failure update message * * @private - * @param {Object} data - * @param {Object[]} data.elements list of mail failure data - * @param {string} data.elements[].message_id ID of related message that + * @param {Object} datas + * @param {Object[]} datas.elements list of mail failure data + * @param {string} datas.elements[].message_id ID of related message that * has a mail failure. - * @param {Array} data.elements[].notifications list of notifications + * @param {Array} datas.elements[].notifications list of notifications * that is related to a mail failure. - * @param {string} data.elements[].notifications[0] sending state of a mail + * @param {string} datas.elements[].notifications[0] sending state of a mail * failure (e.g. 'exception'). */ _handlePartnerMailFailureNotification: function (datas) { @@ -313,6 +313,8 @@ MailManager.include({ this._handlePartnerActivityUpdateNotification(data); } else if (data.type === 'mail_failure') { this._handlePartnerMailFailureNotification(data); + } else if (data.type === 'user_connection') { + this._handlePartnerUserConnectionNotification(data); } else { this._handlePartnerChannelNotification(data); } @@ -402,8 +404,25 @@ MailManager.include({ this._mailBus.trigger('unsubscribe_from_channel', data.id); this.do_notify(_("Unsubscribed"), message); } + }, + /** + * Shows a popup to notify a user connection + * + * @private + * @param {Object} data + * @param {Object[]} data.partner_id id of the connected partner + * @param {string} data.title title to display on notification + * @param {Array} data.messages message to display on notification + */ + _handlePartnerUserConnectionNotification: function (data) { + var self=this; + var partner_id = data.partner_id; + this.call('bus_service', 'sendNotification', data.title, data.message, function(){ + self.call('mail_service', 'openDmWindow', partner_id); + }); }, /** + * * On receiving an update on user status (e.g. becoming 'online', 'offline', * 'idle', etc.). * diff --git a/addons/mail/static/src/js/systray/systray_messaging_menu.js b/addons/mail/static/src/js/systray/systray_messaging_menu.js index 8ab95cc6fab..5552a80393e 100644 --- a/addons/mail/static/src/js/systray/systray_messaging_menu.js +++ b/addons/mail/static/src/js/systray/systray_messaging_menu.js @@ -150,6 +150,27 @@ var MessagingMenu = Widget.extend({ previews: previews, })); }, + /** + * Display the browser notification request dialog when the user clicks on systray's corresponding notification + * + * @private + */ + _requestNotificationPermission: function () { + var self = this; + var def = window.Notification && window.Notification.requestPermission(); + if (def) { + def.then(function (value) { + if (value !== 'granted') { + self.call('bus_service', 'sendNotification', self, _t('Permission denied'), + _t('Odoo will not have the permission to send native notifications on this device.')); + } else { + self.call('bus_service', 'sendNotification', self, _t('Permission granted'), + _t('Odoo has now the permission to send you native notifications on this device.')); + } + }); + } + this.$(".o_mail_navbar_request_permission").slideUp(); + }, /** * Get and render list of previews, based on the selected filter * @@ -269,6 +290,8 @@ var MessagingMenu = Widget.extend({ var documentID = $target.data('document-id'); var documentModel = $target.data('document-model'); this._openDocument(documentModel, documentID); + } else if (previewID === 'request_notification') { + this._requestNotificationPermission(); } else { // preview of thread this.call('mail_service', 'openThread', previewID); diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js index 8cae9862dea..14d50d593ba 100644 --- a/addons/mail/static/tests/document_thread_window_tests.js +++ b/addons/mail/static/tests/document_thread_window_tests.js @@ -84,7 +84,12 @@ QUnit.module('DocumentThreadWindow', { partner_id: partnerID, // so that needaction messages are treated as needactions }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, + afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; + } }); QUnit.test('open a document thread in a thread window', function (assert) { diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js index c1fc1bd8dae..0ef6679c675 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js @@ -58,8 +58,11 @@ QUnit.module('MessagingMenu (Mail Failures)', { }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; // unpatch _.debounce and _.throttle _.debounce = this.underscoreDebounce; _.throttle = this.underscoreThrottle; diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js index 634eef57d18..8c2942be76d 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js @@ -93,8 +93,11 @@ QUnit.module('MessagingMenu', { }, }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; // unpatch _.debounce and _.throttle _.debounce = this.underscoreDebounce; _.throttle = this.underscoreThrottle; diff --git a/addons/mail_bot/__init__.py b/addons/mail_bot/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/mail_bot/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/mail_bot/__manifest__.py b/addons/mail_bot/__manifest__.py new file mode 100644 index 00000000000..01885576d16 --- /dev/null +++ b/addons/mail_bot/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Odoobot', + 'version': '1.0', + 'category': 'Discuss', + 'summary': 'Add Odoobot in discussions', + 'description': "", + 'website': 'https://www.odoo.com/page/discuss', + 'depends': ['mail'], + 'installable': True, + 'application': False, + 'auto_install': True, + 'data': [ + 'views/assets.xml', + 'data/mailbot_data.xml', + ], + 'qweb': [ + 'views/discuss.xml', + ], +} diff --git a/addons/mail_bot/data/mailbot_data.xml b/addons/mail_bot/data/mailbot_data.xml new file mode 100644 index 00000000000..96fe760ca1f --- /dev/null +++ b/addons/mail_bot/data/mailbot_data.xml @@ -0,0 +1,12 @@ + + + + + OdooBot + + OdooBot + + + + + diff --git a/addons/mail_bot/models/__init__.py b/addons/mail_bot/models/__init__.py new file mode 100644 index 00000000000..82aa165ecaf --- /dev/null +++ b/addons/mail_bot/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ir_http +from . import mail_bot +from . import mail_channel +from . import mail_thread +from . import res_partner +from . import res_users diff --git a/addons/mail_bot/models/ir_http.py b/addons/mail_bot/models/ir_http.py new file mode 100644 index 00000000000..ae01cb3e76f --- /dev/null +++ b/addons/mail_bot/models/ir_http.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Http(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + res = super(Http, self).session_info() + res['odoobot_initialized'] = self.env.user.odoobot_initialized + return res diff --git a/addons/mail_bot/models/mail_bot.py b/addons/mail_bot/models/mail_bot.py new file mode 100644 index 00000000000..477e6aab7ee --- /dev/null +++ b/addons/mail_bot/models/mail_bot.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import itertools +import random + +from odoo import models, _ + + +class MailBot(models.AbstractModel): + _name = 'mail.bot' + + def _apply_logic(self, record, values, command=None): + """ Apply bot logic to generate an answer (or not) for the user + The logic will only be applied if odoobot is in a chat with a user or + if someone pinged odoobot. + + :param record: the mail_thread (or mail_channel) where the user + message was posted/odoobot will answer. + :param values: msg_values of the message_post or other values needed by logic + :param command: the name of the called command if the logic is not triggered by a message_post + """ + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + if len(record) != 1 or values.get("author_id") == odoobot_id: + return + if self._is_bot_pinged(values) or self._is_bot_in_private_channel(record): + body = values.get("body", "").replace(u'\xa0', u' ').strip().lower().strip(".?!") + answer = self._get_answer(record, body, values, command) + if answer: + message_type = values.get('message_type', 'comment') + subtype_id = values.get('subtype_id', self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')) + record.with_context({"mail_create_nosubscribe": True}).message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id) + + def _get_answer(self, record, body, values, command=False): + # onboarding + if self._is_bot_in_private_channel(record): + # main flow + if body in ('hi', 'hello'): + return _("Hi, I will guide you through chat features. First, try to send me an emoji 😊") + elif self._body_contains_emoji(body): + return _("Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!") + elif values.get("attachment_ids"): + return _("Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help).") + elif command == 'help': + return _("Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with @OdooBot.") + elif self._is_bot_pinged(values): + discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id + discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"' + chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"' + return _("That's me! 🎉
") + \ + _("There are 3 different ways in Odoo to interact with your colleagues:
\ +-via this chat window
\ +-via the Discuss application:


\ +-or via the chatter:


\ +Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src) + # easter eggs + elif body in ['❤️', _('i love you'), _('love')]: + return _("Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional ❤️") + elif body in [_('help'), _('help me'), _('i need help')]: + return _("I'm trying to help you, but I'm just a bot... :( You can also check our documentation) for more information!") + elif _('fuck') in body or "fuck" in body: + return _("That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! 💔") + else: + return random.choice([ + _("Mmmmh I'm not sure what you mean.. Can you try again?"), + _("I'm afraid I don't understand. Sorry!") + ]) + elif self._is_bot_pinged(values): + return random.choice([_("Yaaaay that's me!"), _("Odeyhoooo")]) + return False + + def _body_contains_emoji(self, body): + # coming from https://unicode.org/emoji/charts/full-emoji-list.html + emoji_list = itertools.chain( + range(0x0030, 0x3a), + range(0x2194, 0x219a), + range(0x21A9, 0x21ab), + range(0x231A, 0x231c), + range(0x23E9, 0x23f4), + range(0x23F8, 0x23fb), + range(0x25AA, 0x25ac), + range(0x25FB, 0x25ff), + range(0x2600, 0x2605), + range(0x2614, 0x2616), + range(0x2622, 0x2624), + range(0x262E, 0x2630), + range(0x2638, 0x263b), + range(0x2648, 0x2654), + range(0x265F, 0x2661), + range(0x2665, 0x2667), + range(0x267E, 0x2680), + range(0x2692, 0x2698), + range(0x269B, 0x269d), + range(0x26A0, 0x26a2), + range(0x26AA, 0x26ac), + range(0x26B0, 0x26b2), + range(0x26BD, 0x26bf), + range(0x26C4, 0x26c6), + range(0x26D3, 0x26d5), + range(0x26E9, 0x26eb), + range(0x26F0, 0x26f6), + range(0x26F7, 0x26fb), + range(0x2708, 0x270a), + range(0x270A, 0x270c), + range(0x270C, 0x270e), + range(0x2733, 0x2735), + range(0x2753, 0x2756), + range(0x2763, 0x2765), + range(0x2795, 0x2798), + range(0x2934, 0x2936), + range(0x2B05, 0x2b08), + range(0x2B1B, 0x2b1d), + range(0x1F170, 0x1f172), + range(0x1F191, 0x1f19b), + range(0x1F1E6, 0x1f200), + range(0x1F201, 0x1f203), + range(0x1F232, 0x1f23b), + range(0x1F250, 0x1f252), + range(0x1F300, 0x1f321), + range(0x1F324, 0x1f32d), + range(0x1F32D, 0x1f330), + range(0x1F330, 0x1f336), + range(0x1F337, 0x1f37d), + range(0x1F37E, 0x1f380), + range(0x1F380, 0x1f394), + range(0x1F396, 0x1f398), + range(0x1F399, 0x1f39c), + range(0x1F39E, 0x1f3a0), + range(0x1F3A0, 0x1f3c5), + range(0x1F3C6, 0x1f3cb), + range(0x1F3CB, 0x1f3cf), + range(0x1F3CF, 0x1f3d4), + range(0x1F3D4, 0x1f3e0), + range(0x1F3E0, 0x1f3f1), + range(0x1F3F3, 0x1f3f6), + range(0x1F3F8, 0x1f400), + range(0x1F400, 0x1f43f), + range(0x1F442, 0x1f4f8), + range(0x1F4F9, 0x1f4fd), + range(0x1F500, 0x1f53e), + range(0x1F549, 0x1f54b), + range(0x1F54B, 0x1f54f), + range(0x1F550, 0x1f568), + range(0x1F56F, 0x1f571), + range(0x1F573, 0x1f57a), + range(0x1F58A, 0x1f58e), + range(0x1F595, 0x1f597), + range(0x1F5B1, 0x1f5b3), + range(0x1F5C2, 0x1f5c5), + range(0x1F5D1, 0x1f5d4), + range(0x1F5DC, 0x1f5df), + range(0x1F5FB, 0x1f600), + range(0x1F601, 0x1f611), + range(0x1F612, 0x1f615), + range(0x1F61C, 0x1f61f), + range(0x1F620, 0x1f626), + range(0x1F626, 0x1f628), + range(0x1F628, 0x1f62c), + range(0x1F62E, 0x1f630), + range(0x1F630, 0x1f634), + range(0x1F635, 0x1f641), + range(0x1F641, 0x1f643), + range(0x1F643, 0x1f645), + range(0x1F645, 0x1f650), + range(0x1F680, 0x1f6c6), + range(0x1F6CB, 0x1f6d0), + range(0x1F6D1, 0x1f6d3), + range(0x1F6E0, 0x1f6e6), + range(0x1F6EB, 0x1f6ed), + range(0x1F6F4, 0x1f6f7), + range(0x1F6F7, 0x1f6f9), + range(0x1F910, 0x1f919), + range(0x1F919, 0x1f91f), + range(0x1F920, 0x1f928), + range(0x1F928, 0x1f930), + range(0x1F931, 0x1f933), + range(0x1F933, 0x1f93b), + range(0x1F93C, 0x1f93f), + range(0x1F940, 0x1f946), + range(0x1F947, 0x1f94c), + range(0x1F94D, 0x1f950), + range(0x1F950, 0x1f95f), + range(0x1F95F, 0x1f96c), + range(0x1F96C, 0x1f971), + range(0x1F973, 0x1f977), + range(0x1F97C, 0x1f980), + range(0x1F980, 0x1f985), + range(0x1F985, 0x1f992), + range(0x1F992, 0x1f998), + range(0x1F998, 0x1f9a3), + range(0x1F9B0, 0x1f9ba), + range(0x1F9C1, 0x1f9c3), + range(0x1F9D0, 0x1f9e7), + range(0x1F9E7, 0x1fa00), + [0x23, 0x2a, 0xa9, 0xae, 0x203c, 0x2049, 0x2122, 0x2139, 0x2328, 0x23cf, 0x24c2, 0x25b6, 0x25c0, 0x260e, 0x2611, + 0x2618, 0x261d, 0x2620, 0x2626, 0x262a, 0x2640, 0x2642, 0x2663, 0x2668, 0x267b, 0x2699, 0x26c8, 0x26ce, 0x26cf, + 0x26d1, 0x26fd, 0x2702, 0x2705, 0x270f, 0x2712, 0x2714, 0x2716, 0x271d, 0x2721, 0x2728, 0x2744, 0x2747, 0x274c, + 0x274e, 0x2757, 0x27a1, 0x27b0, 0x27bf, 0x2b50, 0x2b55, 0x3030, 0x303d, 0x3297, 0x3299, 0x1f004, 0x1f0cf, 0x1f17e, + 0x1f17f, 0x1f18e, 0x1f21a, 0x1f22f, 0x1f321, 0x1f336, 0x1f37d, 0x1f3c5, 0x1f3f7, 0x1f43f, 0x1f440, 0x1f441, 0x1f4f8, + 0x1f4fd, 0x1f4ff, 0x1f57a, 0x1f587, 0x1f590, 0x1f5a4, 0x1f5a5, 0x1f5a8, 0x1f5bc, 0x1f5e1, 0x1f5e3, 0x1f5e8, 0x1f5ef, + 0x1f5f3, 0x1f5fa, 0x1f600, 0x1f611, 0x1f615, 0x1f616, 0x1f617, 0x1f618, 0x1f619, 0x1f61a, 0x1f61b, 0x1f61f, 0x1f62c, + 0x1f62d, 0x1f634, 0x1f6d0, 0x1f6e9, 0x1f6f0, 0x1f6f3, 0x1f6f9, 0x1f91f, 0x1f930, 0x1f94c, 0x1f97a, 0x1f9c0] + ) + if any(chr(emoji) in body for emoji in emoji_list): + return True + return False + + def _is_bot_pinged(self, values): + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + return (4, odoobot_id) in values.get('partner_ids', []) + + def _is_bot_in_private_channel(self, record): + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + if record._name == 'mail.channel' and record.channel_type == 'chat': + return odoobot_id in record.with_context(active_test=False).channel_partner_ids.ids + return False diff --git a/addons/mail_bot/models/mail_channel.py b/addons/mail_bot/models/mail_channel.py new file mode 100644 index 00000000000..23978eff1e6 --- /dev/null +++ b/addons/mail_bot/models/mail_channel.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ + + +class Channel(models.Model): + _inherit = 'mail.channel' + + def _execute_command_help(self, **kwargs): + super(Channel, self)._execute_command_help(**kwargs) + self.env['mail.bot']._apply_logic(self, kwargs, command="help") # kwargs are not usefull but... + + @api.model + def channel_fetch_listeners(self, uuid): + """ Return the id, name and email of partners listening to the given channel """ + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + self._cr.execute(""" + SELECT P.id, P.name, P.email + FROM mail_channel_partner CP + INNER JOIN res_partner P ON CP.partner_id = P.id + INNER JOIN mail_channel C ON CP.channel_id = C.id + WHERE C.uuid = %s OR P.id = %s""", (uuid, odoobot_id,)) + return self._cr.dictfetchall() + + @api.model + def init_odoobot(self): + if not self.env.user.odoobot_initialized: + partner = self.env.user.partner_id + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + channel = self.with_context({"mail_create_nosubscribe": True}).create({ + 'channel_partner_ids': [(4, partner.id), (4, odoobot_id)], + 'public': 'private', + 'channel_type': 'chat', + 'email_send': False, + 'name': 'OdooBot' + }) + message = _("Hello, I'm here to help you discover chat features. Try answering me with an emoji 😊") + channel.message_post(body=message, author_id=odoobot_id, message_type="comment", subtype="mail.mt_comment") + self.env.user.odoobot_initialized = True + return channel diff --git a/addons/mail_bot/models/mail_thread.py b/addons/mail_bot/models/mail_thread.py new file mode 100644 index 00000000000..81e9fa44711 --- /dev/null +++ b/addons/mail_bot/models/mail_thread.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + def _message_post_after_hook(self, message, values, model_description=False, mail_auto_delete=True): + self.env['mail.bot']._apply_logic(self, values) + return super(MailThread, self)._message_post_after_hook(message, values, model_description=model_description, mail_auto_delete=mail_auto_delete) diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py new file mode 100644 index 00000000000..941972a7184 --- /dev/null +++ b/addons/mail_bot/models/res_partner.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, models + +class Partner(models.Model): + _inherit = 'res.partner' + + def _compute_im_status(self): + #we asume that mail_bot _compute_im_status will be executed after bus _compute_im_status + super(Partner, self)._compute_im_status() + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + for partner in self: + if partner.id == odoobot_id: + partner.im_status = 'bot' + + @api.model + def get_mention_suggestions(self, search, limit=8): + #add odoobot in mention suggestion when pinging in mail_thread + [users, partners] = super(Partner, self).get_mention_suggestions(search, limit=limit) + if len(partners) + len(users) < limit and "odoobot".startswith(search.lower()): + odoobot = self.env.ref("mail_bot.partner_odoobot") + if not any([elem['id'] == odoobot.id for elem in partners]): + if odoobot: + partners.append({'id': odoobot.id, 'name': odoobot.name, 'email': odoobot.email}) + return [users, partners] + diff --git a/addons/mail_bot/models/res_users.py b/addons/mail_bot/models/res_users.py new file mode 100644 index 00000000000..8c880b11a6e --- /dev/null +++ b/addons/mail_bot/models/res_users.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + +class Users(models.Model): + _inherit = 'res.users' + + odoobot_initialized = fields.Boolean(readonly=True, default=False) diff --git a/addons/mail_bot/static/img/odoobot_chatter.png b/addons/mail_bot/static/img/odoobot_chatter.png new file mode 100644 index 00000000000..e9147652083 Binary files /dev/null and b/addons/mail_bot/static/img/odoobot_chatter.png differ diff --git a/addons/mail_bot/static/img/odoobot_discuss.png b/addons/mail_bot/static/img/odoobot_discuss.png new file mode 100644 index 00000000000..18949ce9f9e Binary files /dev/null and b/addons/mail_bot/static/img/odoobot_discuss.png differ diff --git a/addons/mail_bot/static/src/js/mailbot_service.js b/addons/mail_bot/static/src/js/mailbot_service.js new file mode 100644 index 00000000000..300e95d06db --- /dev/null +++ b/addons/mail_bot/static/src/js/mailbot_service.js @@ -0,0 +1,28 @@ +odoo.define('mail_bot.MailBotService', function (require) { +"use strict"; + +var AbstractService = require('web.AbstractService'); +var core = require('web.core'); +var session = require('web.session'); + +var MailBotService = AbstractService.extend({ + name: 'mailbot_service', + /** + * @override + */ + start: function () { + var self = this; + if ('odoobot_initialized' in session && ! session.odoobot_initialized) { + setTimeout(function () { + session.odoobot_initialized = true; + self._rpc({ + model: 'mail.channel', + method: 'init_odoobot', + }); + }, 2*60*1000); + } + }, +}); +core.serviceRegistry.add('mailbot_service', MailBotService); +return MailBotService; +}); diff --git a/addons/mail_bot/views/assets.xml b/addons/mail_bot/views/assets.xml new file mode 100644 index 00000000000..134b54e931e --- /dev/null +++ b/addons/mail_bot/views/assets.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml new file mode 100644 index 00000000000..e7f0650c867 --- /dev/null +++ b/addons/mail_bot/views/discuss.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/addons/test_mail/__manifest__.py b/addons/test_mail/__manifest__.py index 40d43883d4b..4f76d51cac9 100644 --- a/addons/test_mail/__manifest__.py +++ b/addons/test_mail/__manifest__.py @@ -9,7 +9,7 @@ 'description': """This module contains tests related to mail. Those are contained in a separate module as it contains models used only to perform tests independently to functional aspects of other models. """, - 'depends': ['test_performance', 'mail'], + 'depends': ['test_performance', 'mail', 'mail_bot'], 'data': [ 'security/ir.model.access.csv', 'data/data.xml', diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index 2d63b95f205..017a983b515 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -15,4 +15,5 @@ from . import test_ir_actions from . import test_update_notification from . import test_discuss from . import test_performance -from . import test_res_users \ No newline at end of file +from . import test_res_users +from . import test_odoobot diff --git a/addons/test_mail/tests/test_odoobot.py b/addons/test_mail/tests/test_odoobot.py new file mode 100644 index 00000000000..b171020244a --- /dev/null +++ b/addons/test_mail/tests/test_odoobot.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.addons.test_mail.tests.common import BaseFunctionalTest, MockEmails, TestRecipients +from odoo.tools import mute_logger +from odoo.tests import tagged + + +@tagged("odoobot") +class TestOdoobot(BaseFunctionalTest, MockEmails, TestRecipients): + + def setUp(self): + super(TestOdoobot, self).setUp() + self.odoobot = self.env.ref("mail_bot.partner_odoobot") + self.message_post_default_kwargs = { + 'body': '', + 'attachment_ids': [], + 'message_type': 'comment', + 'partner_ids': [], + 'subtype': 'mail.mt_comment' + } + self.odoobot_ping_body = '@OdooBot' % (self.odoobot.id, self.odoobot.id) + self.test_record_employe = self.test_record.sudo(self.user_employee) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_odoobot_ping(self): + kwargs = self.message_post_default_kwargs.copy() + kwargs.update({'body': self.odoobot_ping_body, 'partner_ids': [self.odoobot.id, self.user_admin.partner_id.id]}) + + with patch('random.choice', lambda x: x[0]): + self.assertNextMessage( + self.test_record_employe.with_context({'mail_post_autofollow': True}).message_post(**kwargs), + sender=self.odoobot, + answer=["Yaaaay that's me!"] # no the perfect idea to base a test on a random but it should work for test phase, better mock random choice? + ) + # Odoobot should not be a follower but user_employee and user_admin should + follower = self.test_record.message_follower_ids.mapped('partner_id') + self.assertNotIn(self.odoobot, follower) + self.assertIn(self.user_employee.partner_id, follower) + self.assertIn(self.user_admin.partner_id, follower) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_onboarding_flow(self): + kwargs = self.message_post_default_kwargs.copy() + channel = self.env['mail.channel'].sudo(self.user_employee).init_odoobot() + + kwargs['body'] = 'tagada 😊' + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!" + ) + kwargs['body'] = '' + kwargs['attachment_ids'] = [1] + last_message = self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help)." + ) + kwargs['attachment_ids'] = [] + + channel.execute_command(command="help") + self.assertNextMessage( + last_message, # no message will be post with command help, use last odoobot message instead + sender=self.odoobot, + answer="Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with @OdooBot." + ) + # we dont test the end of the flow since it will depends of the installed apps (livechat) + + kwargs['partner_ids'] = [] + kwargs['body'] = "I love you" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional ❤️" + ) + kwargs['body'] = "Go fuck yourself" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! 💔" + ) + # we should have a default answer + with patch('random.choice', lambda x: x[0]): + kwargs['body'] = "I'm batman" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Mmmmh I'm not sure what you mean.. Can you try again?" + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_odoobot_no_default_answer(self): + kwargs = self.message_post_default_kwargs.copy() + kwargs.update({'body': "I'm not talking to @odoobot right now", 'partner_ids': []}) + self.assertNextMessage( + self.test_record_employe.message_post(**kwargs), + answer=False + ) + + def assertNextMessage(self, message, answer=None, sender=None): + last_message = self.env['mail.message'].search([('id', '=', message.id + 1)]) + if last_message: + body = last_message.body.replace('

', '').replace('

', '') + else: + self.assertFalse(answer, "No last message found when an answer was expect") + if answer is not None: + if answer and not last_message: + self.assertTrue(False, "No last message found") + if isinstance(answer, list): + self.assertIn(body, answer) + elif isinstance(answer, tuple): + for elem in answer: + self.assertIn(elem, body) + elif not answer: + self.assertFalse(last_message, "No answer should have been post") + return + else: + self.assertEqual(body, answer) + if sender: + self.assertEqual(sender, last_message.author_id) + return last_message diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 3c6dc33bdc6..b1501bc004a 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -166,7 +166,7 @@ class TestAdvMailPerformance(TransactionCase): 'default_res_model': 'mail.test.activity', }) - with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13 + with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13 activity = MailActivity.create({ 'summary': 'Test Activity', 'res_id': record.id, @@ -182,7 +182,7 @@ class TestAdvMailPerformance(TransactionCase): def test_adv_activity_mixin(self): record = self.env['mail.test.activity'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13 + with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13 record.action_start('Test Start') record.write({'name': 'Dupe write'}) @@ -199,7 +199,7 @@ class TestAdvMailPerformance(TransactionCase): self.user_test.write({'notification_type': 'email'}) record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=56, emp=71): # com runbot: 56 - 71 // test_mail only: 56 - 71 + with self.assertQueryCount(margin=1, admin=56, emp=75): # com runbot: 56 - 75 // test_mail only: 56 - 75 record.write({ 'user_id': self.user_test.id, }) @@ -209,7 +209,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_assignation_inbox(self): record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=36, emp=46): # test_mail only: 36 - 46 + with self.assertQueryCount(margin=1, admin=44, emp=55): # test_mail only: 43 - 54 record.write({ 'user_id': self.user_test.id, }) @@ -253,7 +253,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=47, emp=64): # com runbot: 45 - 62 // test_mail only: 47 - 64 + with self.assertQueryCount(margin=1, admin=47, emp=67): # com runbot: 45 - 65 // test_mail only: 47 - 67 record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, @@ -389,7 +389,7 @@ class TestHeavyMailPerformance(TransactionCase): self.umbrella.message_subscribe(self.user_portal.partner_id.ids) record = self.umbrella.sudo(self.env.user) - with self.assertQueryCount(admin=79, emp=99): # com runbot 77 - 97 // test_mail only: 79 - 99 + with self.assertQueryCount(admin=97, emp=124): # com runbot 90 - 113 // test_mail only: 97 - 120 record.message_post( body='

Test Post Performances

', message_type='comment', @@ -406,7 +406,7 @@ class TestHeavyMailPerformance(TransactionCase): record = self.umbrella.sudo(self.env.user) template_id = self.env.ref('test_mail.mail_test_tpl').id - with self.assertQueryCount(admin=98, emp=130): # com runbot 96 - 128 // test_mail only: 98 - 130 + with self.assertQueryCount(admin=116, emp=155): # com runbot 109 - 144 // test_mail only: 116 - 151 record.message_post_with_template(template_id, message_type='comment', composition_mode='comment') self.assertEqual(record.message_ids[0].body, '

Adding stuff on %s

' % record.name) @@ -476,7 +476,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(admin=58, emp=73): # com runbot: 58 - 73 // test_mail only: 58 - 73 + with self.assertQueryCount(admin=66, emp=88): # com runbot: 65 - 83 // test_mail only: 66 - 84 rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) @@ -499,7 +499,7 @@ class TestHeavyMailPerformance(TransactionCase): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(margin=1, admin=155, emp=184): # com runbot: 155 - 184 // test_mail only: 155 - 184 + with self.assertQueryCount(margin=1, admin=155, emp=190): # com runbot: 155 - 190 // test_mail only: 155 - 190 rec = self.env['mail.test.full'].create({ 'name': 'Test', 'umbrella_id': umbrella_id, @@ -528,7 +528,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(margin=1, admin=94, emp=109): # com runbot: 94 - 109 // test_mail only: 94 - 109 + with self.assertQueryCount(margin=1, admin=105, emp=126): # com runbot: 104 - 123 // test_mail only: 105 - 124 rec.write({ 'name': 'Test2', 'umbrella_id': self.umbrella.id, @@ -566,7 +566,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(margin=1, admin=100, emp=119): # test_mail only: 100 - 119 + with self.assertQueryCount(margin=1, admin=111, emp=137): # test_mail only: 111 - 134 rec.write({ 'name': 'Test2', 'umbrella_id': umbrella_id, @@ -600,7 +600,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) - with self.assertQueryCount(admin=54, emp=75): # test_mail only: 54 - 75 + with self.assertQueryCount(admin=54, emp=76): # test_mail only: 54 - 75 rec.write({ 'name': 'Test2', 'customer_id': customer_id,