diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/im_livechat/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/im_livechat/models')
| -rw-r--r-- | addons/im_livechat/models/__init__.py | 7 | ||||
| -rw-r--r-- | addons/im_livechat/models/digest.py | 50 | ||||
| -rw-r--r-- | addons/im_livechat/models/im_livechat_channel.py | 289 | ||||
| -rw-r--r-- | addons/im_livechat/models/mail_channel.py | 215 | ||||
| -rw-r--r-- | addons/im_livechat/models/rating.py | 27 | ||||
| -rw-r--r-- | addons/im_livechat/models/res_partner.py | 35 | ||||
| -rw-r--r-- | addons/im_livechat/models/res_users.py | 27 |
7 files changed, 650 insertions, 0 deletions
diff --git a/addons/im_livechat/models/__init__.py b/addons/im_livechat/models/__init__.py new file mode 100644 index 00000000..bc392a29 --- /dev/null +++ b/addons/im_livechat/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -* +from . import res_users +from . import res_partner +from . import im_livechat_channel +from . import mail_channel +from . import rating +from . import digest diff --git a/addons/im_livechat/models/digest.py b/addons/im_livechat/models/digest.py new file mode 100644 index 00000000..5b23ab65 --- /dev/null +++ b/addons/im_livechat/models/digest.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_livechat_rating = fields.Boolean('% of Happiness') + kpi_livechat_rating_value = fields.Float(digits=(16, 2), compute='_compute_kpi_livechat_rating_value') + kpi_livechat_conversations = fields.Boolean('Conversations handled') + kpi_livechat_conversations_value = fields.Integer(compute='_compute_kpi_livechat_conversations_value') + kpi_livechat_response = fields.Boolean('Time to answer(sec)', help="Time to answer the user in second.") + kpi_livechat_response_value = fields.Float(compute='_compute_kpi_livechat_response_value') + + def _compute_kpi_livechat_rating_value(self): + channels = self.env['mail.channel'].search([('livechat_operator_id', '=', self.env.user.partner_id.id)]) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + domain = [ + ('create_date', '>=', start), ('create_date', '<', end), + ('rated_partner_id', '=', self.env.user.partner_id.id) + ] + ratings = channels.rating_get_grades(domain) + record.kpi_livechat_rating_value = ratings['great'] * 100 / sum(ratings.values()) if sum(ratings.values()) else 0 + + def _compute_kpi_livechat_conversations_value(self): + for record in self: + start, end, company = record._get_kpi_compute_parameters() + record.kpi_livechat_conversations_value = self.env['mail.channel'].search_count([ + ('channel_type', '=', 'livechat'), + ('livechat_operator_id', '=', self.env.user.partner_id.id), + ('create_date', '>=', start), ('create_date', '<', end) + ]) + + def _compute_kpi_livechat_response_value(self): + for record in self: + start, end, company = record._get_kpi_compute_parameters() + response_time = self.env['im_livechat.report.operator'].sudo().read_group([ + ('start_date', '>=', start), ('start_date', '<', end), + ('partner_id', '=', self.env.user.partner_id.id)], ['partner_id', 'time_to_answer'], ['partner_id']) + record.kpi_livechat_response_value = "%.2f" % sum([response['time_to_answer'] for response in response_time]) or 0 + + def _compute_kpis_actions(self, company, user): + res = super(Digest, self)._compute_kpis_actions(company, user) + res['kpi_livechat_rating'] = 'im_livechat.rating_rating_action_livechat_report' + res['kpi_livechat_conversations'] = 'im_livechat.im_livechat_report_operator_action' + res['kpi_livechat_response'] = 'im_livechat.im_livechat_report_channel_time_to_answer_action' + return res diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py new file mode 100644 index 00000000..0938b6bf --- /dev/null +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 +import random +import re + +from odoo import api, fields, models, modules, _ + + +class ImLivechatChannel(models.Model): + """ Livechat Channel + Define a communication channel, which can be accessed with 'script_external' (script tag to put on + external website), 'script_internal' (code to be integrated with odoo website) or via 'web_page' link. + It provides rating tools, and access rules for anonymous people. + """ + + _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()) + + def _default_user_ids(self): + return [(6, 0, [self._uid])] + + # attribute fields + name = fields.Char('Name', required=True, help="The name of the channel") + button_text = fields.Char('Text of the Button', default='Have a Question? Chat with us.', + help="Default text displayed on the Livechat Support Button") + default_message = fields.Char('Welcome Message', default='How may I help you?', + help="This is an automated 'welcome' message that your visitor will see when they initiate a new conversation.") + input_placeholder = fields.Char('Chat Input Placeholder', help='Text that prompts the user to initiate the chat.') + header_background_color = fields.Char(default="#875A7B", help="Default background color of the channel header once open") + title_color = fields.Char(default="#FFFFFF", help="Default title color of the channel once open") + button_background_color = fields.Char(default="#878787", help="Default background color of the Livechat button") + button_text_color = fields.Char(default="#FFFFFF", help="Default text color of the Livechat button") + + # computed fields + web_page = fields.Char('Web Page', compute='_compute_web_page_link', store=False, readonly=True, + 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) + script_external = fields.Text('Script (external)', compute='_compute_script_external', store=False, readonly=True) + 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) + + # relationnal fields + user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids) + channel_ids = fields.One2many('mail.channel', 'livechat_channel_id', 'Sessions') + rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id', 'Rules') + + def _are_you_inside(self): + for channel in self: + channel.are_you_inside = bool(self.env.uid in [u.id for u in channel.user_ids]) + + def _compute_script_external(self): + view = self.env['ir.model.data'].get_object('im_livechat', 'external_loader') + values = { + "url": self.env['ir.config_parameter'].sudo().get_param('web.base.url'), + "dbname": self._cr.dbname, + } + for record in self: + values["channel_id"] = record.id + record.script_external = view._render(values) if record.id else False + + def _compute_web_page_link(self): + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + for record in self: + record.web_page = "%s/im_livechat/support/%i" % (base_url, record.id) if record.id else False + + @api.depends('channel_ids') + def _compute_nbr_channel(self): + data = self.env['mail.channel'].read_group([ + ('livechat_channel_id', 'in', self._ids), + ('channel_message_ids', '!=', False)], ['__count'], ['livechat_channel_id'], lazy=False) + channel_count = {x['livechat_channel_id'][0]: x['__count'] for x in data} + for record in self: + record.nbr_channel = channel_count.get(record.id, 0) + + # -------------------------- + # Action Methods + # -------------------------- + def action_join(self): + self.ensure_one() + return self.write({'user_ids': [(4, self._uid)]}) + + def action_quit(self): + self.ensure_one() + return self.write({'user_ids': [(3, self._uid)]}) + + def action_view_rating(self): + """ Action to display the rating relative to the channel, so all rating of the + sessions of the current channel + :returns : the ir.action 'action_view_rating' with the correct domain + """ + self.ensure_one() + action = self.env['ir.actions.act_window']._for_xml_id('im_livechat.rating_rating_action_view_livechat_rating') + action['domain'] = [('parent_res_id', '=', self.id), ('parent_res_model', '=', 'im_livechat.channel')] + return action + + # -------------------------- + # 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_mail_channel_vals(self, anonymous_name, operator, user_id=None, country_id=None): + # partner to add to the mail.channel + operator_partner_id = operator.partner_id.id + channel_partner_to_add = [(4, operator_partner_id)] + visitor_user = False + if user_id: + visitor_user = self.env['res.users'].browse(user_id) + if visitor_user and visitor_user.active: # valid session user (not public) + channel_partner_to_add.append((4, visitor_user.partner_id.id)) + return { + 'channel_partner_ids': channel_partner_to_add, + 'livechat_active': True, + 'livechat_operator_id': operator_partner_id, + 'livechat_channel_id': self.id, + 'anonymous_name': False if user_id else anonymous_name, + 'country_id': country_id, + 'channel_type': 'livechat', + 'name': ' '.join([visitor_user.display_name if visitor_user else anonymous_name, operator.livechat_username if operator.livechat_username else operator.name]), + 'public': 'private', + 'email_send': False, + } + + def _open_livechat_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 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 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). + """ + 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 + + # create the session, and add the link with the given channel + mail_channel_vals = self._get_livechat_mail_channel_vals(anonymous_name, operator, user_id=user_id, country_id=country_id) + mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create(mail_channel_vals) + mail_channel._broadcast([operator.partner_id.id]) + return mail_channel.sudo().channel_info()[0] + + 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 c.channel_type = 'livechat' + 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),)) + 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 { + 'header_background_color': self.header_background_color, + 'button_background_color': self.button_background_color, + 'title_color': self.title_color, + 'button_text_color': self.button_text_color, + 'button_text': self.button_text, + 'input_placeholder': self.input_placeholder, + 'default_message': self.default_message, + "channel_name": self.name, + "channel_id": self.id, + } + + def get_livechat_info(self, username='Visitor'): + self.ensure_one() + + if username == 'Visitor': + username = _('Visitor') + info = {} + 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._get_channel_infos() + info['options']['current_partner_id'] = self.env.user.partner_id.id + info['options']["default_username"] = username + return info + + +class ImLivechatChannelRule(models.Model): + """ Channel Rules + Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop' + option to open automatically the conversation. + """ + + _name = 'im_livechat.channel.rule' + _description = 'Livechat Channel Rules' + _order = 'sequence asc' + + regex_url = fields.Char('URL Regex', + help="Regular expression specifying the web pages this rule will be applied on.") + action = fields.Selection([('display_button', 'Display the button'), ('auto_popup', 'Auto popup'), ('hide_button', 'Hide the button')], + string='Action', required=True, default='display_button', + help="* 'Display the button' displays the chat button on the pages.\n"\ + "* 'Auto popup' displays the button and automatically open the conversation pane.\n"\ + "* 'Hide the button' hides the chat button on the pages.") + auto_popup_timer = fields.Integer('Auto popup timer', default=0, + help="Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Auto popup' otherwise this parameter will not be taken into account.") + channel_id = fields.Many2one('im_livechat.channel', 'Channel', + help="The channel of the rule") + country_ids = fields.Many2many('res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Country', + help="The rule will only be applied for these countries. Example: if you select 'Belgium' and 'United States' and that you set the action to 'Hide Button', the chat button will be hidden on the specified URL from the visitors located in these 2 countries. This feature requires GeoIP installed on your server.") + sequence = fields.Integer('Matching order', default=10, + help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.") + + def match_rule(self, channel_id, url, country_id=False): + """ determine if a rule of the given channel matches with the given url + :param channel_id : the identifier of the channel_id + :param url : the url to match with a rule + :param country_id : the identifier of the country + :returns the rule that matches the given condition. False otherwise. + :rtype : im_livechat.channel.rule + """ + def _match(rules): + for rule in rules: + # url might not be set because it comes from referer, in that + # case match the first rule with no regex_url + if re.search(rule.regex_url or '', url or ''): + return rule + return False + # first, search the country specific rules (the first match is returned) + if country_id: # don't include the country in the research if geoIP is not installed + domain = [('country_ids', 'in', [country_id]), ('channel_id', '=', channel_id)] + rule = _match(self.search(domain)) + if rule: + return rule + # second, fallback on the rules without country + domain = [('country_ids', '=', False), ('channel_id', '=', channel_id)] + return _match(self.search(domain)) diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py new file mode 100644 index 00000000..f6c89c36 --- /dev/null +++ b/addons/im_livechat/models/mail_channel.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + +class ChannelPartner(models.Model): + _inherit = 'mail.channel.partner' + + @api.autovacuum + def _gc_unpin_livechat_sessions(self): + """ Unpin livechat sessions with no activity for at least one day to + clean the operator's interface """ + self.env.cr.execute(""" + UPDATE mail_channel_partner + SET is_pinned = false + WHERE id in ( + SELECT cp.id FROM mail_channel_partner cp + INNER JOIN mail_channel c on c.id = cp.channel_id + WHERE c.channel_type = 'livechat' AND cp.is_pinned is true AND + cp.write_date < current_timestamp - interval '1 day' + ) + """) + + +class MailChannel(models.Model): + """ Chat Session + Reprensenting a conversation between users. + It extends the base method for anonymous usage. + """ + + _name = 'mail.channel' + _inherit = ['mail.channel', 'rating.mixin'] + + anonymous_name = fields.Char('Anonymous Name') + channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')]) + livechat_active = fields.Boolean('Is livechat ongoing?', help='Livechat session is active until visitor leave the conversation.') + livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel') + livechat_operator_id = fields.Many2one('res.partner', string='Operator', help="""Operator for this specific channel""") + country_id = fields.Many2one('res.country', string="Country", help="Country of the visitor of the channel") + + _sql_constraints = [('livechat_operator_id', "CHECK((channel_type = 'livechat' and livechat_operator_id is not null) or (channel_type != 'livechat'))", + 'Livechat Operator ID is required for a channel of type livechat.')] + + def _compute_is_chat(self): + super(MailChannel, self)._compute_is_chat() + for record in self: + if record.channel_type == 'livechat': + record.is_chat = True + + def _channel_message_notifications(self, message, message_format=False): + """ When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when + clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header + should be added to the notification, since the user cannot be listining to the channel. + """ + livechat_channels = self.filtered(lambda x: x.channel_type == 'livechat') + other_channels = self.filtered(lambda x: x.channel_type != 'livechat') + notifications = super(MailChannel, livechat_channels)._channel_message_notifications(message.with_context(im_livechat_use_username=True)) + \ + super(MailChannel, other_channels)._channel_message_notifications(message, message_format) + for channel in self: + # add uuid for private livechat channels to allow anonymous to listen + if channel.channel_type == 'livechat' and channel.public == 'private': + notifications.append([channel.uuid, notifications[0][1]]) + if not message.author_id: + unpinned_channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: not cp.is_pinned) + if unpinned_channel_partner: + unpinned_channel_partner.write({'is_pinned': True}) + notifications = self._channel_channel_notifications(unpinned_channel_partner.mapped('partner_id').ids) + notifications + return notifications + + def channel_fetch_message(self, last_id=False, limit=20): + """ Override to add the context of the livechat username.""" + channel = self.with_context(im_livechat_use_username=True) if self.channel_type == 'livechat' else self + return super(MailChannel, channel).channel_fetch_message(last_id=last_id, limit=limit) + + def channel_info(self, extra_info=False): + """ Extends the channel header by adding the livechat operator and the 'anonymous' profile + :rtype : list(dict) + """ + channel_infos = super(MailChannel, self).channel_info(extra_info) + channel_infos_dict = dict((c['id'], c) for c in channel_infos) + for channel in self: + # add the last message date + if channel.channel_type == 'livechat': + # add the operator id + if channel.livechat_operator_id: + res = channel.livechat_operator_id.with_context(im_livechat_use_username=True).name_get()[0] + channel_infos_dict[channel.id]['operator_pid'] = (res[0], res[1].replace(',', '')) + # add the anonymous or partner name + channel_infos_dict[channel.id]['livechat_visitor'] = channel._channel_get_livechat_visitor_info() + return list(channel_infos_dict.values()) + + @api.model + def channel_fetch_slot(self): + values = super(MailChannel, self).channel_fetch_slot() + pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True)]).mapped('channel_id') + values['channel_livechat'] = self.search([('channel_type', '=', 'livechat'), ('id', 'in', pinned_channels.ids)]).channel_info() + return values + + def _channel_get_livechat_visitor_info(self): + self.ensure_one() + # remove active test to ensure public partner is taken into account + channel_partner_ids = self.with_context(active_test=False).channel_partner_ids + partners = channel_partner_ids - self.livechat_operator_id + if not partners: + # operator probably testing the livechat with his own user + partners = channel_partner_ids + first_partner = partners and partners[0] + if first_partner and (not first_partner.user_ids or not any(user._is_public() for user in first_partner.user_ids)): + # legit non-public partner + return { + 'country': first_partner.country_id.name_get()[0] if first_partner.country_id else False, + 'id': first_partner.id, + 'name': first_partner.name, + } + return { + 'country': self.country_id.name_get()[0] if self.country_id else False, + 'id': False, + 'name': self.anonymous_name or _("Visitor"), + } + + def _channel_get_livechat_partner_name(self): + if self.livechat_operator_id in self.channel_partner_ids: + partners = self.channel_partner_ids - self.livechat_operator_id + if partners: + partner_name = False + for partner in partners: + if not partner_name: + partner_name = partner.name + else: + partner_name += ', %s' % partner.name + if partner.country_id: + partner_name += ' (%s)' % partner.country_id.name + return partner_name + if self.anonymous_name: + return self.anonymous_name + return _("Visitor") + + @api.autovacuum + def _gc_empty_livechat_sessions(self): + hours = 1 # never remove empty session created within the last hour + self.env.cr.execute(""" + SELECT id as id + FROM mail_channel C + WHERE NOT EXISTS ( + SELECT * + FROM mail_message_mail_channel_rel R + WHERE R.mail_channel_id = C.id + ) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL AND + COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp + < ((now() at time zone 'UTC') - interval %s)""", ("%s hours" % hours,)) + empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()] + self.browse(empty_channel_ids).unlink() + + def _define_command_history(self): + return { + 'channel_types': ['livechat'], + 'help': _('See 15 last visited pages') + } + + def _execute_command_history(self, **kwargs): + notification = [] + notification_values = { + '_type': 'history_command', + } + notification.append([self.uuid, dict(notification_values)]) + return self.env['bus.bus'].sendmany(notification) + + def _send_history_message(self, pid, page_history): + message_body = _('No history found') + if page_history: + html_links = ['<li><a href="%s" target="_blank">%s</a></li>' % (page, page) for page in page_history] + message_body = '<span class="o_mail_notification"><ul>%s</ul></span>' % (''.join(html_links)) + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', pid), { + 'body': message_body, + 'channel_ids': self.ids, + 'info': 'transient_message', + }) + + def _get_visitor_leave_message(self, operator=False, cancel=False): + return _('Visitor has left the conversation.') + + def _close_livechat_session(self, **kwargs): + """ Set deactivate the livechat channel and notify (the operator) the reason of closing the session.""" + self.ensure_one() + if self.livechat_active: + self.livechat_active = False + # avoid useless notification if the channel is empty + if not self.channel_message_ids: + return + # Notify that the visitor has left the conversation + self.message_post(author_id=self.env.ref('base.partner_root').id, + body=self._get_visitor_leave_message(**kwargs), message_type='comment', subtype_xmlid='mail.mt_comment') + + # Rating Mixin + + def _rating_get_parent_field_name(self): + return 'livechat_channel_id' + + def _email_livechat_transcript(self, email): + company = self.env.user.company_id + render_context = { + "company": company, + "channel": self, + } + template = self.env.ref('im_livechat.livechat_email_template') + mail_body = template._render(render_context, engine='ir.qweb', minimal_qcontext=True) + mail_body = self.env['mail.render.mixin']._replace_local_links(mail_body) + mail = self.env['mail.mail'].sudo().create({ + 'subject': _('Conversation with %s', self.livechat_operator_id.name), + 'email_from': company.catchall_formatted or company.email_formatted, + 'author_id': self.env.user.partner_id.id, + 'email_to': email, + 'body_html': mail_body, + }) + mail.send() diff --git a/addons/im_livechat/models/rating.py b/addons/im_livechat/models/rating.py new file mode 100644 index 00000000..f6a638ed --- /dev/null +++ b/addons/im_livechat/models/rating.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Rating(models.Model): + + _inherit = "rating.rating" + + @api.depends('res_model', 'res_id') + def _compute_res_name(self): + for rating in self: + # cannot change the rec_name of session since it is use to create the bus channel + # so, need to override this method to set the same alternative rec_name as in reporting + if rating.res_model == 'mail.channel': + current_object = self.env[rating.res_model].sudo().browse(rating.res_id) + rating.res_name = ('%s / %s') % (current_object.livechat_channel_id.name, current_object.id) + else: + super(Rating, rating)._compute_res_name() + + def action_open_rated_object(self): + action = super(Rating, self).action_open_rated_object() + if self.res_model == 'mail.channel': + view_id = self.env.ref('im_livechat.mail_channel_view_form').id + action['views'] = [[view_id, 'form']] + return action diff --git a/addons/im_livechat/models/res_partner.py b/addons/im_livechat/models/res_partner.py new file mode 100644 index 00000000..4359b772 --- /dev/null +++ b/addons/im_livechat/models/res_partner.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, api + + +class Partners(models.Model): + """ Update of res.partners class + - override name_get to take into account the livechat username + """ + _inherit = 'res.partner' + + def name_get(self): + if self.env.context.get('im_livechat_use_username'): + # process the ones with livechat username + users_with_livechatname = self.env['res.users'].search([('partner_id', 'in', self.ids), ('livechat_username', '!=', False)]) + map_with_livechatname = {} + for user in users_with_livechatname: + map_with_livechatname[user.partner_id.id] = user.livechat_username + + # process the ones without livecaht username + partner_without_livechatname = self - users_with_livechatname.mapped('partner_id') + no_livechatname_name_get = super(Partners, partner_without_livechatname).name_get() + map_without_livechatname = dict(no_livechatname_name_get) + + # restore order + result = [] + for partner in self: + name = map_with_livechatname.get(partner.id) + if not name: + name = map_without_livechatname.get(partner.id) + result.append((partner.id, name)) + else: + result = super(Partners, self).name_get() + return result diff --git a/addons/im_livechat/models/res_users.py b/addons/im_livechat/models/res_users.py new file mode 100644 index 00000000..e909d0da --- /dev/null +++ b/addons/im_livechat/models/res_users.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + + +class Users(models.Model): + """ Update of res.users class + - add a preference about username for livechat purpose + """ + _inherit = 'res.users' + + livechat_username = fields.Char("Livechat Username", help="This username will be used as your name in the livechat channels.") + + def __init__(self, pool, cr): + """ Override of __init__ to add access rights on livechat_username + Access rights are disabled by default, but allowed + on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. + """ + init_res = super(Users, self).__init__(pool, cr) + # duplicate list to avoid modifying the original reference + type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) + type(self).SELF_WRITEABLE_FIELDS.extend(['livechat_username']) + # duplicate list to avoid modifying the original reference + type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS) + type(self).SELF_READABLE_FIELDS.extend(['livechat_username']) + return init_res |
