diff options
| author | Odoo's Mergebot <xmo+roboto@odoo.com> | 2019-09-17 18:25:40 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-09-17 18:25:40 +0200 |
| commit | 2476be9bf9e94834d59bcb8c8ec28d973fff5786 (patch) | |
| tree | 93b39ef0d83b8eb26052a9fe0da8d48237fd8598 /addons | |
| parent | aa791a646b5d52f8c56b13ac7f01601cbb345d5d (diff) | |
| parent | 83e67319f0d0e7999754562c757a32a9d87e2549 (diff) | |
[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>
Diffstat (limited to 'addons')
22 files changed, 308 insertions, 31 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 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 @@ <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> diff --git a/addons/website_crm/models/crm_lead.py b/addons/website_crm/models/crm_lead.py index 256a597f25a..dc929284842 100644 --- a/addons/website_crm/models/crm_lead.py +++ b/addons/website_crm/models/crm_lead.py @@ -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 diff --git a/addons/website_crm/models/website_visitor.py b/addons/website_crm/models/website_visitor.py index 7bd3570a17a..82fa1367967 100644 --- a/addons/website_crm/models/website_visitor.py +++ b/addons/website_crm/models/website_visitor.py @@ -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 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_crm_sms/__init__.py b/addons/website_crm_sms/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/website_crm_sms/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/website_crm_sms/__manifest__.py b/addons/website_crm_sms/__manifest__.py new file mode 100644 index 00000000000..c4169ef7865 --- /dev/null +++ b/addons/website_crm_sms/__manifest__.py @@ -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, +} diff --git a/addons/website_crm_sms/models/__init__.py b/addons/website_crm_sms/models/__init__.py new file mode 100644 index 00000000000..8c76c6becd2 --- /dev/null +++ b/addons/website_crm_sms/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import website_visitor diff --git a/addons/website_crm_sms/models/website_visitor.py b/addons/website_crm_sms/models/website_visitor.py new file mode 100644 index 00000000000..57ce4281ff0 --- /dev/null +++ b/addons/website_crm_sms/models/website_visitor.py @@ -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 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..48d6d08a847 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') @@ -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 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 @@ <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> diff --git a/addons/website_sms/__init__.py b/addons/website_sms/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/website_sms/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/website_sms/__manifest__.py b/addons/website_sms/__manifest__.py new file mode 100644 index 00000000000..2f950750094 --- /dev/null +++ b/addons/website_sms/__manifest__.py @@ -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, +} diff --git a/addons/website_sms/models/__init__.py b/addons/website_sms/models/__init__.py new file mode 100644 index 00000000000..8c76c6becd2 --- /dev/null +++ b/addons/website_sms/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import website_visitor diff --git a/addons/website_sms/models/website_visitor.py b/addons/website_sms/models/website_visitor.py new file mode 100644 index 00000000000..7042ed22766 --- /dev/null +++ b/addons/website_sms/models/website_visitor.py @@ -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", + } diff --git a/addons/website_sms/views/website_visitor_views.xml b/addons/website_sms/views/website_visitor_views.xml new file mode 100644 index 00000000000..6e30b773cc0 --- /dev/null +++ b/addons/website_sms/views/website_visitor_views.xml @@ -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> |
