diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 78d029689a9..91315809b58 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -74,7 +74,7 @@ class LivechatController(http.Controller): @http.route('/im_livechat/init', type='json', auth="public", cors="*") def livechat_init(self, channel_id): - operator_available = len(request.env['im_livechat.channel'].sudo().browse(channel_id)._get_available_users()) + operator_available = len(request.env['im_livechat.channel'].sudo().browse(channel_id).available_operator_ids) rule = {} # find the country from the request country_id = False diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 92c1c908597..a2e9c40bd1b 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -17,11 +17,7 @@ class ImLivechatChannel(models.Model): _name = 'im_livechat.channel' _inherit = ['rating.parent.mixin'] _description = 'Livechat Channel' - _rating_satisfaction_days = 7 # include only last 7 days to compute satisfaction - - def _default_image(self): - image_path = modules.get_module_resource('im_livechat', 'static/src/img', 'default.png') - return base64.b64encode(open(image_path, 'rb').read()) + _rating_satisfaction_days = 14 # include only last 14 days to compute satisfaction def _default_user_ids(self): return [(6, 0, [self._uid])] @@ -49,10 +45,11 @@ class ImLivechatChannel(models.Model): help="URL to a static page where you client can discuss with the operator of the channel.") are_you_inside = fields.Boolean(string='Are you inside the matrix?', compute='_are_you_inside', store=False, readonly=True) + available_operator_ids = fields.Many2many('res.users', compute='_compute_available_operator_ids') script_external = fields.Html('Script (external)', compute='_compute_script_external', store=False, readonly=True, sanitize=False) nbr_channel = fields.Integer('Number of conversation', compute='_compute_nbr_channel', store=False, readonly=True) - image_128 = fields.Image("Image", max_width=128, max_height=128, default=_default_image) + image_128 = fields.Image("Image", max_width=128, max_height=128) # relationnal fields user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids) @@ -64,6 +61,11 @@ class ImLivechatChannel(models.Model): for channel in self: channel.are_you_inside = bool(self.env.uid in [u.id for u in channel.user_ids]) + @api.depends('user_ids.im_status') + def _compute_available_operator_ids(self): + for record in self: + record.available_operator_ids = record.user_ids.filtered(lambda user: user.im_status == 'online') + @api.depends('rule_ids.chatbot_script_id') def _compute_chatbot_script_count(self): data = self.env['im_livechat.channel.rule']._read_group( @@ -130,13 +132,6 @@ class ImLivechatChannel(models.Model): # -------------------------- # Channel Methods # -------------------------- - 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.user_ids.filtered(lambda user: user.im_status == 'online') - def _get_livechat_discuss_channel_vals(self, anonymous_name, operator=None, chatbot_script=None, user_id=None, country_id=None): # partner to add to the discuss.channel operator_partner_id = operator.partner_id.id if operator else chatbot_script.operator_partner_id.id @@ -190,10 +185,9 @@ class ImLivechatChannel(models.Model): [('channel_id', 'in', self.ids)]).mapped('chatbot_script_id').ids: return False elif 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: - user_operator = next(available_user for available_user in available_users if available_user.partner_id.id == previous_operator_id) + 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() if not user_operator and not chatbot_script: @@ -227,8 +221,7 @@ class ImLivechatChannel(models.Model): :return : user :rtype : res.users """ - operators = self._get_available_users() - if len(operators) == 0: + if not self.available_operator_ids: return False self.env.cr.execute("""SELECT COUNT(DISTINCT c.id), c.livechat_operator_id @@ -238,12 +231,14 @@ class ImLivechatChannel(models.Model): AND c.livechat_operator_id in %s AND m.create_date > ((now() at time zone 'UTC') - interval '30 minutes') GROUP BY c.livechat_operator_id - ORDER BY COUNT(DISTINCT c.id) asc""", (tuple(operators.mapped('partner_id').ids),)) + 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 operators if operator.partner_id.id not in active_channel_operator_ids] + 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) @@ -255,7 +250,7 @@ class ImLivechatChannel(models.Model): 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) + return next(operator for operator in self.available_operator_ids if operator.partner_id.id == less_active_operator) def _get_channel_infos(self): self.ensure_one() @@ -278,7 +273,7 @@ class ImLivechatChannel(models.Model): if username is None: username = _('Visitor') info = {} - info['available'] = self.chatbot_script_count or len(self._get_available_users()) > 0 + info['available'] = self.chatbot_script_count or len(self.available_operator_ids) > 0 info['server_url'] = self.get_base_url() if info['available']: info['options'] = self._get_channel_infos() diff --git a/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss new file mode 100644 index 00000000000..b0ad23d4325 --- /dev/null +++ b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban.scss @@ -0,0 +1,15 @@ +.o_kanban_record:has(.o-livechat-ChannelKanban-highlighted) { + & .oe_kanban_global_click { + background-color: $o-component-active-bg !important; + color: $o-component-active-color; + } +} + +.o-livechat-ChannelKanban-highlighted { + background-color: $o-component-active-bg !important; + color: $o-component-active-color; + + &::before { + background-color: $o-component-active-bg !important; + } +} diff --git a/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml new file mode 100644 index 00000000000..2269a570dcc --- /dev/null +++ b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_menu.xml @@ -0,0 +1,10 @@ + + + + + 'bottom-start' + + + + + diff --git a/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js new file mode 100644 index 00000000000..2534bc36a71 --- /dev/null +++ b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_record.js @@ -0,0 +1,6 @@ +/* @odoo-module */ + +import { KanbanRecord } from "@web/views/kanban/kanban_record"; + +export class LivechatChannelKanbanRecord extends KanbanRecord {} +LivechatChannelKanbanRecord.menuTemplate = "im_livechat.KanbanRecordMenu"; diff --git a/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js new file mode 100644 index 00000000000..7482d962271 --- /dev/null +++ b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_renderer.js @@ -0,0 +1,11 @@ +/* @odoo-module */ + +import { KanbanRenderer } from "@web/views/kanban/kanban_renderer"; +import { LivechatChannelKanbanRecord } from "./livechat_channel_kanban_record"; + +export class LivechatChannelKanbanRenderer extends KanbanRenderer {} + +LivechatChannelKanbanRenderer.components = { + ...KanbanRenderer.components, + KanbanRecord: LivechatChannelKanbanRecord, +}; diff --git a/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js new file mode 100644 index 00000000000..c3bd96d6388 --- /dev/null +++ b/addons/im_livechat/static/src/views/livechat_channel_kanban/livechat_channel_kanban_view.js @@ -0,0 +1,13 @@ +/* @odoo-module */ + +import { LivechatChannelKanbanRenderer } from "@im_livechat/views/livechat_channel_kanban/livechat_channel_kanban_renderer"; + +import { registry } from "@web/core/registry"; +import { kanbanView } from "@web/views/kanban/kanban_view"; + +const livechatChannelKanbanView = { + ...kanbanView, + Renderer: LivechatChannelKanbanRenderer, +}; + +registry.category("views").add("im_livechat.livechat_channel_kanban", livechatChannelKanbanView); 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 index a79bb584ceb..5e12919eb4f 100644 --- 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 @@ -5,13 +5,13 @@ 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`. + * Simulates `_compute_available_operator_ids` on `im_livechat.channel`. * * @private * @param {integer} id * @returns {Object} */ - _mockImLivechatChannel_getAvailableUsers(id) { + _mockImLivechatChannel__computeAvailableOperatorIds(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"); @@ -77,7 +77,7 @@ patch(MockServer.prototype, "im_livechat/models/im_livechat_channel", { * @returns {Object} */ _mockImLivechatChannel_getRandomOperator(id) { - const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id); + const availableUsers = this._mockImLivechatChannel__computeAvailableOperatorIds(id); return availableUsers[0]; }, /** @@ -101,7 +101,7 @@ patch(MockServer.prototype, "im_livechat/models/im_livechat_channel", { ) { let operator; if (previous_operator_id) { - const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id); + const availableUsers = this._mockImLivechatChannel__computeAvailableOperatorIds(id); operator = availableUsers.find((user) => user.partner_id === previous_operator_id); } if (!operator) { diff --git a/addons/im_livechat/tests/common.py b/addons/im_livechat/tests/common.py index 9c23f1e306a..3258256ff49 100644 --- a/addons/im_livechat/tests/common.py +++ b/addons/im_livechat/tests/common.py @@ -42,7 +42,8 @@ class TestImLivechatCommon(TransactionCase): def setUp(self): super().setUp() - def get_available_users(_): - return self.operators + def _compute_available_operator_ids(channel_self): + for record in channel_self: + record.available_operator_ids = type(self).operators - self.patch(type(self.env['im_livechat.channel']), '_get_available_users', get_available_users) + self.patch(type(self.env['im_livechat.channel']), '_compute_available_operator_ids', _compute_available_operator_ids) diff --git a/addons/im_livechat/tests/test_get_discuss_channel.py b/addons/im_livechat/tests/test_get_discuss_channel.py index 5f42e1a35d1..d41dd0be531 100644 --- a/addons/im_livechat/tests/test_get_discuss_channel.py +++ b/addons/im_livechat/tests/test_get_discuss_channel.py @@ -26,7 +26,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): # ensure visitor info are correct with anonymous operator = self.operators[0] - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_discuss_channel(anonymous_name='Visitor 22', previous_operator_id=operator.partner_id.id, country_id=belgium.id) + channel_info = self.livechat_channel.with_user(public_user).sudo()._open_livechat_discuss_channel(anonymous_name='Visitor 22', previous_operator_id=operator.partner_id.id, country_id=belgium.id) self.assertEqual(channel_info['channel']['anonymous_name'], "Visitor 22") self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) @@ -46,7 +46,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): }], key=lambda m: m['id'])) # ensure visitor info are correct with real user - channel_info = self.livechat_channel.with_user(test_user)._open_livechat_discuss_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=test_user.id) + channel_info = self.livechat_channel.with_user(test_user).sudo()._open_livechat_discuss_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=test_user.id) self.assertFalse(channel_info['channel']['anonymous_name']) self.assertEqual(channel_info['channel']['anonymous_country'], [('clear',)]) self.assertEqual(channel_info['channel']['channelMembers'], [('insert', [ @@ -117,7 +117,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): def test_channel_not_pinned_for_operator_before_first_message(self): public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_discuss_channel(anonymous_name='whatever') + channel_info = self.livechat_channel.with_user(public_user).sudo()._open_livechat_discuss_channel(anonymous_name='whatever') operator_channel_member = self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) self.assertEqual(len(operator_channel_member), 1, "operator should be member of channel") self.assertFalse(operator_channel_member.is_pinned, "channel should not be pinned for operator initially") @@ -145,7 +145,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): def test_read_channel_unpined_for_operator_after_one_day(self): public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_discuss_channel(anonymous_name='visitor') + channel_info = self.livechat_channel.with_user(public_user).sudo()._open_livechat_discuss_channel(anonymous_name='visitor') member_of_operator = self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) message = self.env['discuss.channel'].browse(channel_info['id']).message_post(body='cc') member_of_operator.channel_id.with_user(self.operators.filtered( @@ -157,7 +157,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): def test_unread_channel_not_unpined_for_operator_after_autovacuum(self): public_user = self.env.ref('base.public_user') - channel_info = self.livechat_channel.with_user(public_user)._open_livechat_discuss_channel(anonymous_name='visitor') + channel_info = self.livechat_channel.with_user(public_user).sudo()._open_livechat_discuss_channel(anonymous_name='visitor') member_of_operator = self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', 'in', self.operators.partner_id.ids)]) self.env['discuss.channel'].browse(channel_info['id']).message_post(body='cc') with freeze_time(fields.Datetime.to_string(fields.datetime.now() + timedelta(days=1))): diff --git a/addons/im_livechat/tests/test_im_livechat_report.py b/addons/im_livechat/tests/test_im_livechat_report.py index a440a012ab2..a0f8b14461d 100644 --- a/addons/im_livechat/tests/test_im_livechat_report.py +++ b/addons/im_livechat/tests/test_im_livechat_report.py @@ -12,7 +12,11 @@ class TestImLivechatReport(TestImLivechatCommon): super().setUpClass() cls.env['discuss.channel'].search([('livechat_channel_id', '!=', False)]).unlink() - with patch.object(type(cls.env['im_livechat.channel']), '_get_available_users', lambda _: cls.operators): + def _compute_available_operator_ids(channel_self): + for record in channel_self: + record.available_operator_ids = cls.operators + + with patch.object(type(cls.env['im_livechat.channel']), '_compute_available_operator_ids', _compute_available_operator_ids): channel_id = cls.livechat_channel._open_livechat_discuss_channel('Anonymous')['id'] channel = cls.env['discuss.channel'].browse(channel_id) diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 6eed3946487..5d839996086 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -21,7 +21,7 @@ im_livechat.channel.kanban im_livechat.channel - + @@ -30,28 +30,36 @@ + + + + -
-
+
+
Channel
-
- - -
- -
+
- Operators -
- Sessions - + +

Sessions

+
+
+ + +
+
+
+ +
diff --git a/addons/website_livechat/tests/common.py b/addons/website_livechat/tests/common.py index de854f03b70..2703dbb3fde 100644 --- a/addons/website_livechat/tests/common.py +++ b/addons/website_livechat/tests/common.py @@ -61,10 +61,11 @@ class TestLivechatCommon(tests.TransactionCase): self.message_info_url = base_url + "/mail/init_messaging" # override the get_available_users to return only Michel as available - operators = self.operator - def get_available_users(self): - return operators - self.patch(type(self.env['im_livechat.channel']), '_get_available_users', get_available_users) + def _compute_available_operator_ids(channel_self): + for record in channel_self: + record.available_operator_ids = self.operator + + self.patch(type(self.env['im_livechat.channel']), '_compute_available_operator_ids', _compute_available_operator_ids) # override the _get_visitor_from_request to return self.visitor self.target_visitor = self.visitor