[IMP] im_livechat: improve operator assignation

This PR improves operator assignation for livechat channels
by first trying to match an operator with the same lang than
the visitor then an operator of the same country.

part of task-3332872

closes odoo/odoo#125911

Signed-off-by: Didier Debondt (did) <did@odoo.com>
This commit is contained in:
tsm-odoo
2023-07-04 10:16:07 +02:00
parent 51468d3484
commit 00be461524
5 changed files with 143 additions and 23 deletions
+2 -1
View File
@@ -164,7 +164,8 @@ class LivechatController(http.Controller):
chatbot_script=chatbot_script,
user_id=user_id,
country_id=country_id,
persisted=persisted
persisted=persisted,
lang=request.httprequest.cookies.get('frontend_lang')
)
def _post_feedback_message(self, channel, rating, reason):
@@ -329,7 +329,10 @@ class ChatbotScriptStep(models.Model):
posted_message = False
if discuss_channel.livechat_channel_id:
human_operator = discuss_channel.livechat_channel_id._get_random_operator()
human_operator = discuss_channel.livechat_channel_id._get_random_operator(
lang=discuss_channel.livechat_visitor_id.lang_id.code,
country_id=discuss_channel.country_id.id
)
# handle edge case where we found yourself as available operator -> don't do anything
# it will act as if no-one is available (which is fine)
@@ -162,7 +162,7 @@ class ImLivechatChannel(models.Model):
'name': name,
}
def _open_livechat_discuss_channel(self, anonymous_name, previous_operator_id=None, chatbot_script=None, user_id=None, country_id=None, persisted=True):
def _open_livechat_discuss_channel(self, anonymous_name, previous_operator_id=None, chatbot_script=None, user_id=None, country_id=None, persisted=True, lang=None):
""" Return a livechat session. If the session is persisted, creates a discuss.channel record with a connected operator or with Odoobot as
an operator if a chatbot has been configured, or return false otherwise
:param anonymous_name : the name of the anonymous person of the session
@@ -171,6 +171,7 @@ class ImLivechatChannel(models.Model):
:param user_id : the id of the logged in visitor, if any
:param country_code : the country of the anonymous person of the session
:param persisted: whether or not the session should be persisted
:param lang: code of the preferred lang of the visitor.
:type anonymous_name : str
:return : channel header
:rtype : dict
@@ -189,7 +190,7 @@ class ImLivechatChannel(models.Model):
if previous_operator_id in self.available_operator_ids.mapped('partner_id').ids:
user_operator = next(available_user for available_user in self.available_operator_ids if available_user.partner_id.id == previous_operator_id)
if not user_operator and not chatbot_script:
user_operator = self._get_random_operator()
user_operator = self._get_random_operator(lang=lang, country_id=country_id)
if not user_operator and not chatbot_script:
# no one available
return False
@@ -211,13 +212,39 @@ class ImLivechatChannel(models.Model):
'chatbot_script_id': chatbot_script.id if chatbot_script else None
}
def _get_random_operator(self):
def _get_less_active_operator(self, active_channels, operators):
""" Get the operator with the less active conversations.
:param active_channels: list of active livechats (with at least
one message within the last 30 minutes)
:param operators: list of operators to choose from
"""
if not operators:
return False
active_channels = [active_channel for active_channel in active_channels if active_channel['livechat_operator_id'] in operators.mapped('partner_id').ids]
active_operators = [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_operators]
if inactive_operators:
return random.choice(inactive_operators)
# 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_random_operator(self, lang=None, country_id=None):
""" 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.
This method will try to match the given lang and country_id.
(Some annoying conversions have to be made on the fly because this model holds 'res.users' as available operators
and the discuss_channel model stores the partner_id of the randomly selected operator)
:param lang: code of the preferred lang of the visitor.
:param country_id: id of the country of the visitor.
:return : user
:rtype : res.users
"""
@@ -233,24 +260,21 @@ class ImLivechatChannel(models.Model):
GROUP BY c.livechat_operator_id
ORDER BY COUNT(DISTINCT c.id) asc""", (tuple(self.available_operator_ids.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 self.available_operator_ids 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 self.available_operator_ids if operator.partner_id.id == less_active_operator)
operator = None
# Try to match an operator with the same lang as the visitor
if lang:
same_lang_operator_ids = self.available_operator_ids.filtered(lambda operator: operator.partner_id.lang == lang)
if same_lang_operator_ids:
operator = self._get_less_active_operator(active_channels, same_lang_operator_ids)
# Try to match an operator with the same country as the visitor
if country_id and not operator:
same_country_operator_ids = self.available_operator_ids.filtered(lambda operator: operator.partner_id.country_id.id == country_id)
if same_country_operator_ids:
operator = self._get_less_active_operator(active_channels, same_country_operator_ids)
# Try to get a random operator, regardless of the lang or the country
if not operator:
operator = self._get_less_active_operator(active_channels, self.available_operator_ids)
return operator
def _get_channel_infos(self):
self.ensure_one()
+1
View File
@@ -6,6 +6,7 @@ from . import test_chatbot_form_ui
from . import test_chatbot_internals
from . import test_digest
from . import test_get_discuss_channel
from . import test_get_random_operator
from . import test_im_livechat_report
from . import test_im_livechat_support_page
from . import test_performance
@@ -0,0 +1,91 @@
import odoo
from odoo.tests import HttpCase
from odoo.tests.common import new_test_user
from unittest.mock import patch
from odoo.addons.bus.models.res_users import ResUsers
def _compute_im_status(self):
for record in self:
record.im_status = 'online'
@odoo.tests.tagged('-at_install', 'post_install')
@patch.object(ResUsers, '_compute_im_status', _compute_im_status)
class TestGetRandomOperator(HttpCase):
def _create_operator(self, lang_code=None, country_code=None):
operator = new_test_user(self.env, login=f'operator_{lang_code or country_code}_{self.operator_id}')
operator.partner_id = self.env['res.partner'].create({
'name': f'Operator {lang_code or country_code}',
'lang': lang_code,
'country_id': self.env['res.country'].search([('code', '=', country_code)]).id if country_code else None,
})
self.operator_id += 1
return operator
def setUp(self):
super().setUp()
self.operator_id = 0
self.env['res.lang'].with_context(active_test=False).search([
('code', 'in', ['fr_FR', 'es_ES', 'de_DE', 'en_US'])
]).write({'active': True})
def test_get_by_lang(self):
fr_operator = self._create_operator('fr_FR')
en_operator = self._create_operator('en_US')
livechat_channel = self.env['im_livechat.channel'].create({
'name': 'Livechat Channel',
'user_ids': [fr_operator.id, en_operator.id],
})
self.assertEqual(fr_operator, livechat_channel._get_random_operator(lang='fr_FR'))
self.assertEqual(en_operator, livechat_channel._get_random_operator(lang='en_US'))
def test_get_by_lang_no_operator_matching_lang(self):
fr_operator = self._create_operator('fr_FR')
livechat_channel = self.env['im_livechat.channel'].create({
'name': 'Livechat Channel',
'user_ids': [fr_operator.id],
})
self.assertEqual(fr_operator, livechat_channel._get_random_operator(lang='en_US'))
def test_get_by_country(self):
fr_operator = self._create_operator(country_code='FR')
en_operator = self._create_operator(country_code='US')
livechat_channel = self.env['im_livechat.channel'].create({
'name': 'Livechat Channel',
'user_ids': [fr_operator.id, en_operator.id],
})
self.assertEqual(
fr_operator,
livechat_channel._get_random_operator(country_id=self.env['res.country'].search([('code', '=', 'FR')]).id)
)
self.assertEqual(
en_operator,
livechat_channel._get_random_operator(country_id=self.env['res.country'].search([('code', '=', 'US')]).id)
)
def test_get_by_country_no_operator_matching_country(self):
fr_operator = self._create_operator(country_code='FR')
livechat_channel = self.env['im_livechat.channel'].create({
'name': 'Livechat Channel',
'user_ids': [fr_operator.id],
})
self.assertEqual(
fr_operator,
livechat_channel._get_random_operator(country_id=self.env['res.country'].search([('code', '=', 'US')]).id)
)
def test_get_by_lang_and_country_prioritize_lang(self):
fr_operator = self._create_operator('fr_FR', 'FR')
en_operator = self._create_operator('en_US', 'US')
livechat_channel = self.env['im_livechat.channel'].create({
'name': 'Livechat Channel',
'user_ids': [fr_operator.id, en_operator.id],
})
self.assertEqual(
fr_operator,
livechat_channel._get_random_operator(lang='fr_FR', country_id=self.env['res.country'].search([('code', '=', 'US')]).id)
)
self.assertEqual(
en_operator,
livechat_channel._get_random_operator(lang='en_US', country_id=self.env['res.country'].search([('code', '=', 'FR')]).id)
)