From b2247ab72ffaea247a1dce3bf85cba3296e78c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Mon, 16 Sep 2019 10:04:52 +0000 Subject: [PATCH 1/9] [FIX] im_livechat: show message history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revision on https://github.com/odoo/odoo/commit/923fb7aa05f0e9e0e67440073993ec36bb0f5f53 Commit above fixed an issue since v11.0 that display duplicated messages on a website livechat window. Forward-ported broke the livechat in v12.0 due to refactoring of mail https://github.com/odoo/odoo/commit/cd34f6de727d5b3858420cff3c9d3c5995c4e75c This commit correctly adapts fix for v12.0+. opw-2070911 closes odoo/odoo#36921 Signed-off-by: Alexandre Kühn (aku) --- addons/im_livechat/static/src/js/im_livechat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 5a893f2499a..f3ad30c1390 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -125,7 +125,7 @@ var LivechatButton = Widget.extend({ var message = new WebsiteLivechatMessage(this, data, options); var hasAlreadyMessage = _.some(this._messages, function (msg) { - return message.id === msg.id; + return message.getID() === msg.getID(); }); if (hasAlreadyMessage) { return; From a9a1181434b08eb51b99df5e8800267c86e158ed Mon Sep 17 00:00:00 2001 From: David Beguin Date: Fri, 30 Aug 2019 10:06:51 +0000 Subject: [PATCH 2/9] [IMP] crm : factorise get_lead_by_confidence_level to prepare next commits In order to prepare the send mail to visitor that are linked to leads, getting lead by confidence level must be factorised to avoid code duplicates. Task ID: 2056080 PR #36290 --- addons/crm/models/crm_lead.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index a165880e4e7..abe9e285c82 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -764,12 +764,7 @@ class Lead(models.Model): if len(self.ids) <= 1: raise UserError(_('Please select more than one element (lead or opportunity) from the list view.')) - # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it - # The confidence level increases with the stage sequence - # An Opportunity always has higher confidence level than a lead - def opps_key(opportunity): - return opportunity.type == 'opportunity', opportunity.stage_id.sequence, -opportunity.id - opportunities = self.sorted(key=opps_key, reverse=True) + opportunities = self._sort_by_confidence_level(reverse=True) # get SORTED recordset of head and tail, and complete list opportunities_head = opportunities[0] @@ -803,6 +798,16 @@ class Lead(models.Model): return opportunities_head + def _sort_by_confidence_level(self, reverse=False): + """ Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it + The confidence level increases with the stage sequence + An Opportunity always has higher confidence level than a lead + """ + def opps_key(opportunity): + return opportunity.type == 'opportunity', opportunity.stage_id.sequence, -opportunity._origin.id + + return self.sorted(key=opps_key, reverse=reverse) + def get_duplicated_leads(self, partner_id, include_lost=False): """ Search for opportunities that have the same partner and that arent done or cancelled :param partner_id : partner to search From 6f5abf933b62558423a91f999d9aaa261f6847ad Mon Sep 17 00:00:00 2001 From: David Beguin Date: Wed, 28 Aug 2019 14:06:50 +0000 Subject: [PATCH 3/9] [IMP] website,livechat : apply various imp and fix on visitor views Applies various improvements: - Remove welcome message if chat request usecase. - If a visitor is online: line is green / if offline: line is red (in the list view) - Visitors list view: add first / last connection fields and remove time_since_last_action - Avoid useless leave notification if the livechat channel is empty Fix - ACLS on website_visitor_page model (for im_livechat_group_user) - ACLS on website_visitor and website_visitor_page model (for sales_team.group_sale_salesman) - Update visitor lang if visitor change the website lang. - Create visitor twice when translated website (due to rerouting) - Avatar for visitor banner in discuss. (image_64 instead of old image_small) Task ID: 2056080 PR #36290 --- .../im_livechat/static/src/js/im_livechat.js | 4 +++- addons/website/models/ir_http.py | 20 +++++++++++++++---- addons/website/models/website_visitor.py | 9 +++++++-- .../website/views/website_visitor_views.xml | 15 ++++++++++++-- .../website_crm/security/ir.model.access.csv | 1 + addons/website_livechat/controllers/main.py | 2 +- .../models/im_livechat_channel.py | 2 +- .../website_livechat/models/mail_channel.py | 10 +++++++++- .../security/ir.model.access.csv | 1 + .../static/src/xml/thread.xml | 2 +- 10 files changed, 53 insertions(+), 13 deletions(-) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index f3ad30c1390..182dd403954 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -240,7 +240,9 @@ var LivechatButton = Widget.extend({ data: livechatData }); return self._openChatWindow().then(function () { - self._sendWelcomeMessage(); + if (!self._history) { + self._sendWelcomeMessage(); + } self._renderMessages(); self.call('bus_service', 'addChannel', self._livechat.getUUID()); self.call('bus_service', 'startPolling'); diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index da99aa35287..ccfc2c531e4 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -12,13 +12,12 @@ import werkzeug.routing import werkzeug.utils import odoo -from odoo import api, models, registry, fields +from odoo import api, models, registry from odoo import SUPERUSER_ID from odoo.http import request -from odoo.tools import config, ormcache +from odoo.tools import config from odoo.tools.safe_eval import safe_eval from odoo.osv.expression import FALSE_DOMAIN, OR -from odoo.osv import osv from odoo.addons.base.models.qweb import QWebException from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype @@ -100,8 +99,21 @@ class Http(models.AbstractModel): @classmethod def _dispatch(cls): + """ + In case of rerouting for translate (e.g. when visiting odoo.com/fr_BE/), + _dispatch calls reroute() that returns _dispatch with altered request properties. + The second _dispatch will continue until end of process. When second _dispatch is finished, the first _dispatch + call receive the new altered request and continue. + At the end, 2 calls of _dispatch (and this override) are made with exact same request properties, instead of one. + As the response has not been sent back to the client, the visitor cookie does not exist yet when second _dispatch call + is treated in _handle_webpage_dispatch, leading to create 2 visitors with exact same properties. + To avoid this, we check if, !!! before calling super !!!, we are in a rerouting request. If not, it means that we are + handling the original request, in which we should create the visitor. We ignore every other rerouting requests. + """ + is_rerouting = hasattr(request, 'routing_iteration') response = super(Http, cls)._dispatch() - cls._register_website_track(response) + if not is_rerouting: + cls._register_website_track(response) return response @classmethod diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index d2d9ca0bf7d..b42669bc04c 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -147,6 +147,8 @@ class WebsiteVisitor(models.Model): response.set_cookie('visitor_id', visitor_sudo.access_token, expires=expiration_date) else: visitor_sudo._add_tracking(domain, website_track_values) + if visitor_sudo.lang_id.id != request.lang.id: + visitor_sudo.write({'lang_id': request.lang.id}) def _add_tracking(self, domain, website_track_values): """ Update the visitor when a website_track is added""" @@ -155,8 +157,10 @@ class WebsiteVisitor(models.Model): if not last_view or last_view.visit_datetime < datetime.now() - timedelta(minutes=30): website_track_values['visitor_id'] = self.id self.env['website.track'].create(website_track_values) - self.env['website.visitor.lastconnection'].create({'visitor_id': self.id, - 'connection_datetime': website_track_values['visit_datetime']}) + self.env['website.visitor.lastconnection'].create({ + 'visitor_id': self.id, + 'connection_datetime': website_track_values['visit_datetime'] + }) def _create_visitor(self, website_track_values=None): """ Create a visitor and add a track to it if website_track_values is set.""" @@ -165,6 +169,7 @@ class WebsiteVisitor(models.Model): vals = { 'lang_id': request.lang.id, 'country_id': country_id, + 'website_id': request.website.id, 'last_connections_ids': [(0, 0, {'connection_datetime': website_track_values['visit_datetime'] or datetime.now()})], } if not self.env.user._is_public(): diff --git a/addons/website/views/website_visitor_views.xml b/addons/website/views/website_visitor_views.xml index c82b66bbe99..2fd70b1b0c2 100644 --- a/addons/website/views/website_visitor_views.xml +++ b/addons/website/views/website_visitor_views.xml @@ -162,11 +162,12 @@ website.visitor.view.tree website.visitor - + - + + @@ -210,6 +211,16 @@ + + website.visitor.view.graph + website.visitor + + + + + + + Visitors ir.actions.act_window diff --git a/addons/website_crm/security/ir.model.access.csv b/addons/website_crm/security/ir.model.access.csv index c85af9f0e0c..d2dd311b15c 100644 --- a/addons/website_crm/security/ir.model.access.csv +++ b/addons/website_crm/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_website_visitor_salesman,access_website_visitor_salesman,model_website_visitor,sales_team.group_sale_salesman,1,0,0,0 +access_website_visitor_page_salesman,access_website_visitor_page_salesman,model_website_visitor_page,sales_team.group_sale_salesman,1,0,0,0 diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py index 4525191b916..9fb4678d39e 100644 --- a/addons/website_livechat/controllers/main.py +++ b/addons/website_livechat/controllers/main.py @@ -64,7 +64,7 @@ class WebsiteLivechat(http.Controller): in conversation with an operator, it's not possible to send the visitor a chat request.""" mail_channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)]) if mail_channel: - mail_channel.close_livechat_request_session(_('has left the conversation.')) + mail_channel.close_livechat_request_session() @http.route('/im_livechat/close_empty_livechat', type='json', auth="public") def close_empty_livechat(self, uuid): diff --git a/addons/website_livechat/models/im_livechat_channel.py b/addons/website_livechat/models/im_livechat_channel.py index db41f626a83..dd083c6e104 100644 --- a/addons/website_livechat/models/im_livechat_channel.py +++ b/addons/website_livechat/models/im_livechat_channel.py @@ -20,6 +20,6 @@ class ImLivechatChannel(models.Model): # As chat requested by the visitor, delete the chat requested by an operator if any to avoid conflicts between two flows chat_request_channel = self.env['mail.channel'].sudo().search([('livechat_visitor_id', '=', visitor_sudo.id), ('livechat_active', '=', True)]) for mail_channel in chat_request_channel: - mail_channel.close_livechat_request_session(_('has started a conversation with %s. The chat request has been canceled.') % operator.name) + mail_channel.close_livechat_request_session(type='cancel', speaking_with=operator.name) return mail_channel_vals diff --git a/addons/website_livechat/models/mail_channel.py b/addons/website_livechat/models/mail_channel.py index e6d00133eff..795fe9bc2d6 100644 --- a/addons/website_livechat/models/mail_channel.py +++ b/addons/website_livechat/models/mail_channel.py @@ -58,12 +58,20 @@ class MailChannel(models.Model): break return ' → '.join(history) - def close_livechat_request_session(self, message): + def close_livechat_request_session(self, type='leave', **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 name = _('The visitor') if not self.livechat_visitor_id else self.livechat_visitor_id.name + if type == 'cancel': + message = _('has started a conversation with %s. The chat request has been canceled.') % kwargs.get('speaking_with', 'an operator') + else: + message = _('has left the conversation.') leave_message = '%s %s' % (name, message) self.message_post(author_id=self.env.ref('base.user_root').sudo().partner_id.id, body=leave_message, message_type='comment', subtype='mt_comment') diff --git a/addons/website_livechat/security/ir.model.access.csv b/addons/website_livechat/security/ir.model.access.csv index 90de53247be..ce1863efbbc 100644 --- a/addons/website_livechat/security/ir.model.access.csv +++ b/addons/website_livechat/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_im_livechat_channel_public,im_livechat.channel.public,im_livechat.model_im_livechat_channel,,1,0,0,0 access_website_visitor_livechat_users,website.visitor.livechat.users,model_website_visitor,im_livechat.im_livechat_group_user,1,0,0,0 +access_website_track_livechat_users,website.track.livechat.users,website.model_website_track,im_livechat.im_livechat_group_user,1,0,0,0 diff --git a/addons/website_livechat/static/src/xml/thread.xml b/addons/website_livechat/static/src/xml/thread.xml index a8872290b2a..b257f5264b3 100644 --- a/addons/website_livechat/static/src/xml/thread.xml +++ b/addons/website_livechat/static/src/xml/thread.xml @@ -32,7 +32,7 @@
From 73505303f88194b47d2cb7ab9013edc4e9c1210b Mon Sep 17 00:00:00 2001 From: David Beguin Date: Wed, 28 Aug 2019 15:25:45 +0000 Subject: [PATCH 4/9] [IMP] website : send mail to visitor linked to res.partner This commit allows to send an email to a visitor directly from the visitor form view if the visitor is linked a res.partner that a valid email address Task ID: 2056080 PR #36290 --- addons/website/models/website_visitor.py | 70 +++++++++++++++++-- .../website/views/website_visitor_views.xml | 9 ++- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index b42669bc04c..db4a50af825 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -2,12 +2,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, timedelta -from hashlib import sha256 -import hmac import uuid from odoo import fields, models, api, _ -from odoo.tools.misc import _consteq, _format_time_ago +from odoo.exceptions import UserError +from odoo.tools.misc import _format_time_ago from odoo.http import request from odoo.osv import expression @@ -42,17 +41,25 @@ class WebsiteVisitor(models.Model): active = fields.Boolean('Active', default=True) website_id = fields.Many2one('website', "Website", readonly=True) user_partner_id = fields.Many2one('res.partner', string="Linked Partner", help="Partner of the last logged in user.") - create_date = fields.Datetime('First connection date', readonly=True) - last_connection_datetime = fields.Datetime('Last Connection', compute="_compute_time_statistics", help="Last page view date", search='_search_last_connection', readonly=True) - last_connections_ids = fields.One2many('website.visitor.lastconnection', 'visitor_id', readonly=True) + + # localisation and info country_id = fields.Many2one('res.country', 'Country', readonly=True) country_flag = fields.Binary(related="country_id.image", string="Country Flag") lang_id = fields.Many2one('res.lang', string='Language', help="Language from the website when visitor has been created") + email = fields.Char(string='Email', compute='_compute_email_phone') + mobile = fields.Char(string='Mobile Phone', compute='_compute_email_phone') + + # Visit fields visit_count = fields.Integer('Number of visits', default=1, readonly=True, help="A new visit is considered if last connection was more than 24 hours ago.") website_track_ids = fields.One2many('website.track', 'visitor_id', string='Visited Pages History', readonly=True) visitor_page_count = fields.Integer('Page Views', compute="_compute_page_statistics", help="Total number of visits on tracked pages") page_ids = fields.Many2many('website.page', string="Visited Pages", compute="_compute_page_statistics") page_count = fields.Integer('# Visited Pages', compute="_compute_page_statistics", help="Total number of tracked page visited") + + # Time fields + create_date = fields.Datetime('First connection date', readonly=True) + last_connection_datetime = fields.Datetime('Last Connection', compute="_compute_time_statistics", help="Last page view date", search='_search_last_connection', readonly=True) + last_connections_ids = fields.One2many('website.visitor.lastconnection', 'visitor_id', readonly=True) time_since_last_action = fields.Char('Last action', compute="_compute_time_statistics", help='Time since last page view. E.g.: 2 minutes ago') is_connected = fields.Boolean('Is connected ?', compute='_compute_time_statistics', help='A visitor is considered as connected if his last page view was within the last 5 minutes.') @@ -77,6 +84,23 @@ class WebsiteVisitor(models.Model): """ % (operator,) return [('id', 'inselect', (query, [value]))] + @api.depends('user_partner_id.email_normalized', 'user_partner_id.mobile') + def _compute_email_phone(self): + results = self.env['res.partner'].search_read( + [('id', 'in', self.user_partner_id.ids)], + ['id', 'email_normalized', 'mobile'], + ) + mapped_data = { + result['id']: { + 'email_normalized': result['email_normalized'], + 'mobile': result['mobile'] + } for result in results + } + + for visitor in self: + visitor.email = mapped_data.get(visitor.user_partner_id.id, {}).get('email_normalized') + visitor.mobile = mapped_data.get(visitor.user_partner_id.id, {}).get('mobile') + @api.depends('website_track_ids') def _compute_page_statistics(self): results = self.env['website.track'].read_group( @@ -107,6 +131,40 @@ class WebsiteVisitor(models.Model): visitor.time_since_last_action = _format_time_ago(self.env, (datetime.now() - last_connection_datetime)) visitor.is_connected = (datetime.now() - last_connection_datetime) < timedelta(minutes=5) + def _prepare_visitor_send_mail_values(self): + if self.user_partner_id.email: + return { + 'res_model': 'res.partner', + 'res_id': self.user_partner_id.id, + 'partner_ids': [self.user_partner_id.id], + } + return {} + + def action_send_mail(self): + self.ensure_one() + visitor_mail_values = self._prepare_visitor_send_mail_values() + if not visitor_mail_values: + raise UserError(_("There is no email linked this visitor.")) + compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) + ctx = dict( + default_model=visitor_mail_values.get('res_model'), + default_res_id=visitor_mail_values.get('res_id'), + default_use_template=False, + default_partner_ids=[(6, 0, visitor_mail_values.get('partner_ids'))], + default_composition_mode='comment', + default_reply_to=self.env.user.partner_id.email, + ) + return { + 'name': _('Compose Email'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(compose_form.id, 'form')], + 'view_id': compose_form.id, + 'target': 'new', + 'context': ctx, + } + def _get_visitor_from_request(self, with_previous_visitors=False): if not request: return None diff --git a/addons/website/views/website_visitor_views.xml b/addons/website/views/website_visitor_views.xml index 2fd70b1b0c2..a3f97a7ba84 100644 --- a/addons/website/views/website_visitor_views.xml +++ b/addons/website/views/website_visitor_views.xml @@ -85,6 +85,7 @@ +
@@ -114,7 +115,10 @@ website.visitor
-
+
+