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 diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 5a893f2499a..182dd403954 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; @@ -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..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 @@ -147,6 +205,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 +215,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 +227,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..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
-
+
+