diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py
index b3783d2f59d..5703563e95d 100644
--- a/addons/im_livechat/controllers/main.py
+++ b/addons/im_livechat/controllers/main.py
@@ -49,13 +49,12 @@ class LivechatController(http.Controller):
def loader(self, channel_id, **kwargs):
username = kwargs.get("username", _("Visitor"))
channel = request.env['im_livechat.channel'].sudo().browse(channel_id)
- info = request.env['im_livechat.channel'].get_livechat_info(channel.id, username=username)
+ info = channel.get_livechat_info(username=username)
return request.render('im_livechat.loader', {'info': info, 'web_session_required': True}, headers=[('Content-Type', 'application/javascript')])
@http.route('/im_livechat/init', type='json', auth="public", cors="*")
def livechat_init(self, channel_id):
- LivechatChannel = request.env['im_livechat.channel']
- available = len(LivechatChannel.browse(channel_id).get_available_users())
+ available = len(request.env['im_livechat.channel'].sudo().browse(channel_id)._get_available_users())
rule = {}
if available:
# find the country from the request
@@ -81,7 +80,7 @@ class LivechatController(http.Controller):
}
@http.route('/im_livechat/get_session', type="json", auth='public', cors="*")
- def get_session(self, channel_id, anonymous_name, **kwargs):
+ def get_session(self, channel_id, anonymous_name, previous_operator_id=None, **kwargs):
user_id = None
country_id = None
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
@@ -95,7 +94,11 @@ class LivechatController(http.Controller):
country = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1) if country_code else None
if country:
anonymous_name, country_id = _("%s (%s)") % (anonymous_name, country.name), country.id
- return request.env["im_livechat.channel"].with_context(lang=False).get_mail_channel(channel_id, anonymous_name, user_id, country_id)
+
+ if previous_operator_id:
+ previous_operator_id = int(previous_operator_id)
+
+ return request.env["im_livechat.channel"].with_context(lang=False).sudo().browse(channel_id)._get_mail_channel(anonymous_name, previous_operator_id, user_id, country_id)
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
def feedback(self, uuid, rate, reason=None, **kwargs):
diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py
index df57a6c770e..6a568bbb269 100644
--- a/addons/im_livechat/models/im_livechat_channel.py
+++ b/addons/im_livechat/models/im_livechat_channel.py
@@ -124,31 +124,41 @@ class ImLivechatChannel(models.Model):
# Channel Methods
# --------------------------
@api.multi
- def get_available_users(self):
+ def _get_available_users(self):
""" get available user of a given channel
:retuns : return the res.users having their im_status online
"""
self.ensure_one()
- return self.sudo().user_ids.filtered(lambda user: user.im_status == 'online')
+ return self.user_ids.filtered(lambda user: user.im_status == 'online')
- @api.model
- def get_mail_channel(self, livechat_channel_id, anonymous_name, user_id=None, country_id=None):
+ @api.multi
+ def _get_mail_channel(self, anonymous_name, previous_operator_id=None, user_id=None, country_id=None):
""" Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise
- :param livechat_channel_id : the identifier if the im_livechat.channel
:param anonymous_name : the name of the anonymous person of the channel
+ :param previous_operator_id : partner_id.id of the previous operator that this visitor had in the past
:param user_id : the id of the logged in visitor, if any
:param country_code : the country of the anonymous person of the channel
- :type livechat_channel_id : int
:type anonymous_name : str
:return : channel header
:rtype : dict
+
+ If this visitor already had an operator within the last 7 days (information stored with the 'im_livechat_previous_operator_pid' cookie),
+ the system will first try to assign that operator if he's available (to improve user experience).
"""
- # get the avalable user of the channel
- operators = self.sudo().browse(livechat_channel_id).get_available_users()
- if len(operators) == 0:
+ self.ensure_one()
+
+ operator = False
+ if previous_operator_id:
+ available_users = self._get_available_users()
+ # previous_operator_id is the partner_id of the previous operator, need to convert to user
+ if previous_operator_id in available_users.mapped('partner_id').ids:
+ operator = next(available_user for available_user in available_users if available_user.partner_id.id == previous_operator_id)
+ if not operator:
+ operator = self._get_random_operator()
+ if not operator:
+ # no one available
return False
- # choose the res.users operator and get its partner id
- operator = random.choice(operators)
+
operator_partner_id = operator.partner_id.id
# partner to add to the mail.channel
channel_partner_to_add = [(4, operator_partner_id)]
@@ -158,7 +168,7 @@ class ImLivechatChannel(models.Model):
mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
'channel_partner_ids': channel_partner_to_add,
'livechat_operator_id': operator_partner_id,
- 'livechat_channel_id': livechat_channel_id,
+ 'livechat_channel_id': self.id,
'anonymous_name': False if user_id else anonymous_name,
'country_id': country_id,
'channel_type': 'livechat',
@@ -169,24 +179,66 @@ class ImLivechatChannel(models.Model):
mail_channel._broadcast([operator_partner_id])
return mail_channel.sudo().channel_info()[0]
- @api.model
- def get_channel_infos(self, channel_id):
- channel = self.browse(channel_id)
+ def _get_random_operator(self):
+ """ Return a random operator from the available users of the channel that have the lowest number of active livechats.
+ A livechat is considered 'active' if it has at least one message within the 30 minutes.
+
+ (Some annoying conversions have to be made on the fly because this model holds 'res.users' as available operators
+ and the mail_channel model stores the partner_id of the randomly selected operator)
+
+ :return : user
+ :rtype : res.users
+ """
+ operators = self._get_available_users()
+ if len(operators) == 0:
+ return False
+
+ self.env.cr.execute("""SELECT COUNT(DISTINCT c.id), c.livechat_operator_id
+ FROM mail_channel c
+ LEFT OUTER JOIN mail_message_mail_channel_rel r ON c.id = r.mail_channel_id
+ LEFT OUTER JOIN mail_message m ON r.mail_message_id = m.id
+ WHERE m.create_date > ((now() at time zone 'UTC') - interval '30 minutes')
+ AND c.channel_type = 'livechat'
+ AND c.livechat_operator_id in %s
+ GROUP BY c.livechat_operator_id
+ ORDER BY COUNT(DISTINCT c.id) asc""", (tuple(operators.mapped('partner_id').ids),))
+ active_channels = self.env.cr.dictfetchall()
+
+ # If inactive operator(s), return one of them
+ active_channel_operator_ids = [active_channel['livechat_operator_id'] for active_channel in active_channels]
+ inactive_operators = [operator for operator in operators if operator.partner_id.id not in active_channel_operator_ids]
+ if inactive_operators:
+ return random.choice(inactive_operators)
+
+ # If no inactive operator, active_channels is not empty as len(operators) > 0 (see above).
+ # Get the less active operator using the active_channels first element's count (since they are sorted 'ascending')
+ lowest_number_of_conversations = active_channels[0]['count']
+ less_active_operator = random.choice([
+ active_channel['livechat_operator_id'] for active_channel in active_channels
+ if active_channel['count'] == lowest_number_of_conversations])
+
+ # convert the selected 'partner_id' to its corresponding res.users
+ return next(operator for operator in operators if operator.partner_id.id == less_active_operator)
+
+ def _get_channel_infos(self):
+ self.ensure_one()
+
return {
- 'button_text': channel.button_text,
- 'input_placeholder': channel.input_placeholder,
- 'default_message': channel.default_message,
- "channel_name": channel.name,
- "channel_id": channel.id,
+ 'button_text': self.button_text,
+ 'input_placeholder': self.input_placeholder,
+ 'default_message': self.default_message,
+ "channel_name": self.name,
+ "channel_id": self.id,
}
- @api.model
- def get_livechat_info(self, channel_id, username='Visitor'):
+ def get_livechat_info(self, username='Visitor'):
+ self.ensure_one()
+
info = {}
- info['available'] = len(self.browse(channel_id).get_available_users()) > 0
+ info['available'] = len(self._get_available_users()) > 0
info['server_url'] = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
if info['available']:
- info['options'] = self.sudo().get_channel_infos(channel_id)
+ info['options'] = self._get_channel_infos()
info['options']["default_username"] = username
return info
diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index 4e0a48f4325..086f02daa32 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -215,6 +215,7 @@ var LivechatButton = Widget.extend({
def = session.rpc('/im_livechat/get_session', {
channel_id : this.options.channel_id,
anonymous_name : this.options.default_username,
+ previous_operator_id: this._get_previous_operator_id(),
}, {shadow: true});
}
def.then(function (livechatData) {
@@ -233,6 +234,13 @@ var LivechatButton = Widget.extend({
utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
+ if (livechatData.operator_pid[0]) {
+ // livechatData.operator_pid contains a tuple (id, name)
+ // we are only interested in the id
+ var operatorPidId = livechatData.operator_pid[0];
+ var oneWeek = 7*24*60*60;
+ utils.set_cookie('im_livechat_previous_operator_pid', operatorPidId, oneWeek);
+ }
});
}
}).then(function () {
@@ -241,6 +249,24 @@ var LivechatButton = Widget.extend({
self._openingChat = false;
});
}, 200, true),
+ /**
+ * Will try to get a previous operator for this visitor.
+ * If the visitor already had visitor A, it's better for his user experience
+ * to get operator A again.
+ *
+ * The information is stored in the 'im_livechat_previous_operator_pid' cookie.
+ *
+ * @private
+ * @return {integer} operator_id.partner_id.id if the cookie is set
+ */
+ _get_previous_operator_id: function () {
+ var cookie = utils.get_cookie('im_livechat_previous_operator_pid');
+ if (cookie) {
+ return cookie;
+ }
+
+ return null;
+ },
/**
* @private
* @return {Promise}
diff --git a/addons/im_livechat/tests/__init__.py b/addons/im_livechat/tests/__init__.py
new file mode 100644
index 00000000000..a22a67032d1
--- /dev/null
+++ b/addons/im_livechat/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_get_mail_channel
diff --git a/addons/im_livechat/tests/test_get_mail_channel.py b/addons/im_livechat/tests/test_get_mail_channel.py
new file mode 100644
index 00000000000..ac3d5807333
--- /dev/null
+++ b/addons/im_livechat/tests/test_get_mail_channel.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestGetMailChannel(TransactionCase):
+ def setUp(self):
+ super(TestGetMailChannel, self).setUp()
+ self.operators = self.env['res.users'].create([{
+ 'name': 'Michel',
+ 'login': 'michel'
+ }, {
+ 'name': 'Paul',
+ 'login': 'paul'
+ }, {
+ 'name': 'Pierre',
+ 'login': 'pierre'
+ }, {
+ 'name': 'Jean',
+ 'login': 'jean'
+ }, {
+ 'name': 'Georges',
+ 'login': 'georges'
+ }])
+
+ self.livechat_channel = self.env['im_livechat.channel'].create({
+ 'name': 'The channel',
+ 'user_ids': (6, 0, self.operators.ids)
+ })
+
+ operators = self.operators
+ def get_available_users(self):
+ return operators
+
+ self.patch(type(self.env['im_livechat.channel']), '_get_available_users', get_available_users)
+
+ def test_get_mail_channel(self):
+ """For a livechat with 5 available operators, we open 5 channels 5 times (25 channels total).
+ For every 5 channels opening, we check that all operators were assigned.
+ """
+
+ for i in range(5):
+ mail_channels = self._get_mail_channels()
+ channel_operators = [channel_info['operator_pid'] for channel_info in mail_channels]
+ channel_operator_ids = [channel_operator[0] for channel_operator in channel_operators]
+ self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids))
+
+ def _get_mail_channels(self):
+ mail_channels = []
+
+ for i in range(5):
+ mail_channel = self.livechat_channel._get_mail_channel('Anonymous')
+ mail_channels.append(mail_channel)
+ # send a message to mark this channel as 'active'
+ self.env['mail.channel'].browse(mail_channel['id']).write({
+ 'channel_message_ids': [(0, 0, {'body': 'cc'})]
+ })
+
+ return mail_channels
diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml
index 6cc5f093969..130e9d85478 100644
--- a/addons/im_livechat/views/im_livechat_channel_templates.xml
+++ b/addons/im_livechat/views/im_livechat_channel_templates.xml
@@ -123,6 +123,7 @@
+
@@ -141,6 +142,7 @@
+
@@ -187,10 +189,6 @@
-
-
-
-
document.addEventListener("DOMContentLoaded", function(event) {
odoo.define('web.session', function (require) {
diff --git a/addons/website_livechat/models/website.py b/addons/website_livechat/models/website.py
index e207482416a..c52dc9a09f0 100644
--- a/addons/website_livechat/models/website.py
+++ b/addons/website_livechat/models/website.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from odoo import api, fields, models
class Website(models.Model):
@@ -9,3 +9,13 @@ class Website(models.Model):
_inherit = "website"
channel_id = fields.Many2one('im_livechat.channel', string='Website Live Chat Channel')
+
+ @api.multi
+ def get_livechat_channel_info(self):
+ """ Get the livechat info dict (button text, channel name, ...) for the livechat channel of
+ the current website.
+ """
+ self.ensure_one()
+ if self.channel_id:
+ return self.channel_id.sudo().get_livechat_info()
+ return {}
diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml
index e5f429fc806..82580fc6549 100644
--- a/addons/website_livechat/views/website_livechat.xml
+++ b/addons/website_livechat/views/website_livechat.xml
@@ -49,7 +49,7 @@