[MERGE][ADD-IMP] website_* : apply multiple improvements on visitor
PURPOSE
Perform a back2basics on recently-merged and recently-under-merged website
visitor concept. Main points to add are sending emails and sms to visitors
as well as improving integration.
SPECIFICATIONS
Applies various improvements:
- Add graph view on visitors;
- 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;
- Delete the first message "hello, how may i help you?" on a live chat
request when I start a live chat;
- Send mail to visitor linked to res.partner or linked to a crm.lead. New
simplified wizard to post_message on lead or on res_partner if any
(priority to lead). If the lead has a res_partner and not the visitor, set
the partner on the visitor If the lead has no partner, create a partner
based on the lead and link it to the visitor;
- send a sms to a visitor if the visitor is linked to a least 1 lead that
has a mobile number or a phone number or is linked to a res.partner with
mobile number. As in contact form from website, there is only a phone field
and not a mobile one, we allow to send sms if the lead phone field is
filled;
Fixes :
- ACLS on website_visitor_page model (for im_livechat_group_user)
- Update visitor lang if visitor change the website lang.
- Create visitor twice when translated website (due to rerouting)
See sub commit for more informations
LINKS
Task ID : 2070272
closes odoo/odoo#36290
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<field name="is_connected"/>
|
||||
<field name="display_name"/>
|
||||
<field name="page_ids"/>
|
||||
<field name="user_partner_id"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
@@ -114,7 +115,10 @@
|
||||
<field name="model">website.visitor</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Website Visitor" create="0">
|
||||
<header></header>
|
||||
<header>
|
||||
<button name="action_send_mail" type="object" class="btn btn-primary"
|
||||
attrs="{'invisible': [('email', '=', False)]}" string="Send Email"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button o_stat_button_info" attrs="{'invisible': [('is_connected', '=', False)]}">
|
||||
@@ -143,9 +147,10 @@
|
||||
<group string="Visitor Informations">
|
||||
<field name="is_connected" invisible="1"/>
|
||||
<field name="user_partner_id" attrs="{'invisible': [('user_partner_id', '=', False)]}"/>
|
||||
<field name="email"/>
|
||||
<field name="mobile"/>
|
||||
<field name="country_id" attrs="{'invisible': [('country_id', '=', False)]}"/>
|
||||
<field name="lang_id"/>
|
||||
|
||||
</group>
|
||||
<group id="visits" string="Visits">
|
||||
<field name="create_date"/>
|
||||
@@ -162,11 +167,12 @@
|
||||
<field name="name">website.visitor.view.tree</field>
|
||||
<field name="model">website.visitor</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Web Visitors" create="0">
|
||||
<tree string="Web Visitors" create="0" decoration-success="is_connected" decoration-danger="not is_connected">
|
||||
<!--TODO DBE : Handle no_label in treeview-->
|
||||
<field name="country_flag" widget="image" options='{"size": [20, 20]}' string=" "/>
|
||||
<field name="display_name"/>
|
||||
<field name="time_since_last_action"/>
|
||||
<field name="create_date"/>
|
||||
<field name="last_connection_datetime"/>
|
||||
<field name="lang_id"/>
|
||||
<field name="visit_count"/>
|
||||
<field name="page_ids" widget="many2many_tags"/>
|
||||
@@ -210,6 +216,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="website_visitor_view_graph" model="ir.ui.view">
|
||||
<field name="name">website.visitor.view.graph</field>
|
||||
<field name="model">website.visitor</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Visitors">
|
||||
<field name="create_date" type="row"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="website_visitors_action" model="ir.actions.act_window">
|
||||
<field name="name">Visitors</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
||||
@@ -31,7 +31,7 @@ class Lead(models.Model):
|
||||
action = self.env.ref('website.website_visitor_page_action').read()[0]
|
||||
action['domain'] = [('visitor_id', 'in', visitors.ids)]
|
||||
# avoid grouping if only few records
|
||||
if len(visitors.visitor_page_ids.ids) > 15 and len(visitors.page_ids.ids) > 1:
|
||||
if len(visitors.website_track_ids.ids) > 15 and len(visitors.page_ids.ids) > 1:
|
||||
action['context'] = {'search_default_group_by_page': '1'}
|
||||
return action
|
||||
|
||||
|
||||
@@ -14,3 +14,47 @@ class WebsiteVisitor(models.Model):
|
||||
def _compute_lead_count(self):
|
||||
for visitor in self:
|
||||
visitor.lead_count = len(visitor.lead_ids)
|
||||
|
||||
@api.depends('user_partner_id.email_normalized', 'user_partner_id.mobile', 'lead_ids.email_normalized', 'lead_ids.mobile')
|
||||
def _compute_email_phone(self):
|
||||
super(WebsiteVisitor, self)._compute_email_phone()
|
||||
self.flush()
|
||||
sql = """ SELECT v.id as visitor_id, l.id as lead_id,
|
||||
CASE WHEN p.email_normalized is not null THEN p.email_normalized ELSE l.email_normalized END as email,
|
||||
CASE WHEN p.mobile is not null THEN p.mobile WHEN l.mobile is not null THEN l.mobile ELSE l.phone END as mobile
|
||||
FROM website_visitor v
|
||||
JOIN crm_lead_website_visitor_rel lv on lv.website_visitor_id = v.id
|
||||
JOIN crm_lead l ON lv.crm_lead_id = l.id
|
||||
LEFT JOIN res_partner p on p.id = v.user_partner_id
|
||||
WHERE v.id in %s
|
||||
ORDER BY l.create_date ASC"""
|
||||
self.env.cr.execute(sql, (tuple(self.ids),))
|
||||
results = self.env.cr.dictfetchall()
|
||||
mapped_data = {}
|
||||
for result in results:
|
||||
visitor_info = mapped_data.get(result['visitor_id'], {'email': '', 'mobile': ''})
|
||||
visitor_info['email'] += result['email'] + ','
|
||||
visitor_info['mobile'] = result['mobile']
|
||||
mapped_data[result['visitor_id']] = visitor_info
|
||||
|
||||
for visitor in self:
|
||||
email = mapped_data.get(visitor.id, {}).get('email')
|
||||
visitor.email = email[:-1] if email else False
|
||||
visitor.mobile = mapped_data.get(visitor.id, {}).get('mobile')
|
||||
|
||||
def _prepare_visitor_send_mail_values(self):
|
||||
visitor_mail_values = super(WebsiteVisitor, self)._prepare_visitor_send_mail_values()
|
||||
if self.lead_ids:
|
||||
lead = self.lead_ids._sort_by_confidence_level(reverse=True)[0]
|
||||
partner_id = self.user_partner_id.id
|
||||
if not self.user_partner_id:
|
||||
partner_id = lead.handle_partner_assignation()[lead.id]
|
||||
if not lead.partner_id:
|
||||
lead.partner_id = partner_id
|
||||
self.user_partner_id = partner_id
|
||||
return {
|
||||
'res_model': 'crm.lead',
|
||||
'res_id': lead.id,
|
||||
'partner_ids': [partner_id],
|
||||
}
|
||||
return visitor_mail_values
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Send SMS to Visitor with leads',
|
||||
'category': 'Website',
|
||||
'sequence': 54,
|
||||
'summary': 'Allows to send sms to website visitor that have lead',
|
||||
'version': '1.0',
|
||||
'description': """Allows to send sms to website visitor if the visitor is linked to a lead.""",
|
||||
'depends': ['website_sms', 'crm'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import website_visitor
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class WebsiteVisitor(models.Model):
|
||||
_inherit = 'website.visitor'
|
||||
|
||||
def _prepare_visitor_send_sms_values(self):
|
||||
visitor_sms_values = super(WebsiteVisitor, self)._prepare_visitor_send_sms_values()
|
||||
if not visitor_sms_values:
|
||||
leads_with_number = self.lead_ids.filtered(lambda l: l.mobile == self.mobile or l.phone == self.mobile)._sort_by_confidence_level(reverse=True)
|
||||
if leads_with_number:
|
||||
lead = leads_with_number[0]
|
||||
return {
|
||||
'res_model': 'crm.lead',
|
||||
'res_id': lead.id,
|
||||
'partner_ids': [lead.id],
|
||||
'number_field_name': 'mobile' if lead.mobile == self.mobile else 'phone',
|
||||
}
|
||||
return visitor_sms_values
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -76,7 +84,8 @@ class MailChannel(models.Model):
|
||||
message_author_id = message.author_id
|
||||
visitor = self.livechat_visitor_id
|
||||
if len(self) == 1 and visitor and message_author_id != self.livechat_operator_id:
|
||||
visitor.sudo().write({
|
||||
'last_connection_datetime': fields.datetime.now(),
|
||||
self.env['website.visitor.lastconnection'].create({
|
||||
'visitor_id': visitor.id,
|
||||
'connection_datetime': fields.datetime.now()
|
||||
})
|
||||
return message
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -32,7 +32,7 @@
|
||||
<div class="o_thread_message_sidebar align-self-center">
|
||||
<div class="o_thread_message_sidebar_image">
|
||||
<img alt=""
|
||||
t-att-src="visitor.partner_id ? '/web/image/res.partner/' + visitor.partner_id + '/image_small' : '/mail/static/src/img/smiley/avatar.jpg'"
|
||||
t-att-src="visitor.partner_id ? '/web/image/res.partner/' + visitor.partner_id + '/image_64' : '/mail/static/src/img/smiley/avatar.jpg'"
|
||||
class="o_thread_message_avatar rounded-circle"/>
|
||||
<span>
|
||||
<i t-if="visitor.is_connected" aria-label="Visitor is connected" class="o_w_visitor_status o_w_visitor_connected fa fa-circle" role="img" title="Online"></i>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Send SMS to Visitor',
|
||||
'category': 'Website',
|
||||
'sequence': 54,
|
||||
'summary': 'Allows to send sms to website visitor',
|
||||
'version': '1.0',
|
||||
'description': """Allows to send sms to website visitor if the visitor is linked to a partner.""",
|
||||
'depends': ['website', 'sms'],
|
||||
'data': [
|
||||
'views/website_visitor_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import website_visitor
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class WebsiteVisitor(models.Model):
|
||||
_inherit = 'website.visitor'
|
||||
|
||||
def _prepare_visitor_send_sms_values(self):
|
||||
if self.user_partner_id.mobile:
|
||||
return {
|
||||
'res_model': 'res.partner',
|
||||
'res_id': self.user_partner_id.id,
|
||||
'partner_ids': [self.user_partner_id.id],
|
||||
'number_field_name': 'mobile',
|
||||
}
|
||||
return {}
|
||||
|
||||
def action_send_sms(self):
|
||||
self.ensure_one()
|
||||
visitor_sms_values = self._prepare_visitor_send_sms_values()
|
||||
if not visitor_sms_values:
|
||||
raise UserError(_("There is no mobile phone number linked to this visitor."))
|
||||
|
||||
context = dict(self.env.context)
|
||||
context.update({
|
||||
'default_res_model': visitor_sms_values['res_model'],
|
||||
'default_res_id': visitor_sms_values['res_id'],
|
||||
'default_composition_mode': 'comment',
|
||||
'default_number_field_name': visitor_sms_values['number_field_name'],
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "sms.composer",
|
||||
"view_mode": 'form',
|
||||
"context": context,
|
||||
"name": "Send SMS Text Message",
|
||||
"target": "new",
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
<record id="website_visitor_view_form" model="ir.ui.view">
|
||||
<field name="name">website.visitor.view.form.inherit.website.mass.mailing.sms</field>
|
||||
<field name="model">website.visitor</field>
|
||||
<field name="inherit_id" ref="website.website_visitor_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_send_sms" type="object" class="btn btn-primary"
|
||||
attrs="{'invisible': [('mobile', '=', False)]}" string="Send SMS"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
Reference in New Issue
Block a user