From b0991dc110c1757e3a972bc1cebbb33cd06acf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Fri, 18 Mar 2022 10:54:12 +0000 Subject: [PATCH] [IMP] crm_livechat: allow creating leads within the chatbot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PURPOSE This commits adds a special step_type on the chatbot.script.steps that allows creating a crm.lead when reaching it. SPECS In addition, we let the end user configure in which crm.team that lead will land upon being created, making it possible to sort your leads based on previous questions. e.g: "In which country do you live" And based on the answer, you assign to a different sales.team. This commit comes with the necessary business code to make this feature work as well as some extra testing methods that ensure it is correctly implemented. Task-2030386 Part-of: odoo/odoo#84000 Co-authored-by: Patrick Hoste Co-authored-by: Aurélien Warnon --- addons/crm_livechat/__manifest__.py | 3 + .../data/crm_livechat_chatbot_data.xml | 49 ++++++++++++++ addons/crm_livechat/models/__init__.py | 2 + addons/crm_livechat/models/chatbot_script.py | 28 ++++++++ .../models/chatbot_script_step.py | 65 ++++++++++++++++++ addons/crm_livechat/models/mail_channel.py | 6 +- addons/crm_livechat/tests/__init__.py | 2 + addons/crm_livechat/tests/chatbot_common.py | 66 +++++++++++++++++++ .../crm_livechat/tests/test_chatbot_lead.py | 51 ++++++++++++++ .../views/chatbot_script_step_views.xml | 15 +++++ .../views/chatbot_script_views.xml | 18 +++++ .../website_crm_livechat/models/__init__.py | 1 + .../models/chatbot_script_step.py | 16 +++++ 13 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 addons/crm_livechat/data/crm_livechat_chatbot_data.xml create mode 100644 addons/crm_livechat/models/chatbot_script.py create mode 100644 addons/crm_livechat/models/chatbot_script_step.py create mode 100644 addons/crm_livechat/tests/chatbot_common.py create mode 100644 addons/crm_livechat/tests/test_chatbot_lead.py create mode 100644 addons/crm_livechat/views/chatbot_script_step_views.xml create mode 100644 addons/crm_livechat/views/chatbot_script_views.xml create mode 100644 addons/website_crm_livechat/models/chatbot_script_step.py diff --git a/addons/crm_livechat/__manifest__.py b/addons/crm_livechat/__manifest__.py index d43908c47ea..91ee112bc14 100644 --- a/addons/crm_livechat/__manifest__.py +++ b/addons/crm_livechat/__manifest__.py @@ -7,6 +7,9 @@ 'summary': 'Create lead from livechat conversation', 'data': [ 'data/utm_data.xml', + 'data/crm_livechat_chatbot_data.xml', + 'views/chatbot_script_views.xml', + 'views/chatbot_script_step_views.xml', ], 'depends': [ 'crm', diff --git a/addons/crm_livechat/data/crm_livechat_chatbot_data.xml b/addons/crm_livechat/data/crm_livechat_chatbot_data.xml new file mode 100644 index 00000000000..a47411dd0ff --- /dev/null +++ b/addons/crm_livechat/data/crm_livechat_chatbot_data.xml @@ -0,0 +1,49 @@ + + + + + + + Lead Generation Bot + + + + + Hi there, what brings you to our website today? 👋 + 1 + free_input_multi + + + + + 2 + forward_operator + + + + + Hu-ho, it looks like none of our operators are available 🙁 + 3 + text + + + + + Would you mind leaving your email address so that we can reach you back? + 4 + question_email + + + + + Thank you, you should hear back from us very soon! + 5 + create_lead + + + + diff --git a/addons/crm_livechat/models/__init__.py b/addons/crm_livechat/models/__init__.py index 58536ecc3e7..c1a45883018 100644 --- a/addons/crm_livechat/models/__init__.py +++ b/addons/crm_livechat/models/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import chatbot_script +from . import chatbot_script_step from . import mail_channel diff --git a/addons/crm_livechat/models/chatbot_script.py b/addons/crm_livechat/models/chatbot_script.py new file mode 100644 index 00000000000..8fff92a94dd --- /dev/null +++ b/addons/crm_livechat/models/chatbot_script.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class ChatbotScript(models.Model): + _inherit = 'chatbot.script' + + lead_count = fields.Integer( + string='Generated Lead Count', compute='_compute_lead_count') + + def _compute_lead_count(self): + mapped_leads = {} + if self.ids: + leads_data = self.env['crm.lead'].with_context(active_test=False).sudo()._read_group( + [('source_id', 'in', self.mapped('source_id').ids)], ['source_id'], ['source_id']) + mapped_leads = {lead['source_id'][0]: lead['source_id_count'] for lead in leads_data} + + for script in self: + script.lead_count = mapped_leads.get(script.source_id.id, 0) + + def action_view_leads(self): + self.ensure_one() + action = self.env['ir.actions.act_window']._for_xml_id('crm.crm_lead_all_leads') + action['domain'] = [('source_id', '=', self.source_id.id)] + action['context'] = {'create': False} + return action diff --git a/addons/crm_livechat/models/chatbot_script_step.py b/addons/crm_livechat/models/chatbot_script_step.py new file mode 100644 index 00000000000..0c9e21cecc6 --- /dev/null +++ b/addons/crm_livechat/models/chatbot_script_step.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, models, fields + + +class ChatbotScriptStep(models.Model): + _inherit = 'chatbot.script.step' + + step_type = fields.Selection( + selection_add=[('create_lead', 'Create Lead')], ondelete={'create_lead': 'cascade'}) + crm_team_id = fields.Many2one( + 'crm.team', string='Sales Team', ondelete='set null', + help="Used in combination with 'create_lead' step type in order to automatically " + "assign the created lead/opportunity to the defined team") + + def _chatbot_crm_prepare_lead_values(self, mail_channel, description): + return { + 'description': description + mail_channel._get_channel_history(), + 'name': _("%s's New Lead", self.chatbot_script_id.title), + 'source_id': self.chatbot_script_id.source_id.id, + 'team_id': self.crm_team_id.id, + 'type': 'lead' if self.crm_team_id.use_leads else 'opportunity', + 'user_id': False, + } + + def _process_step(self, mail_channel): + self.ensure_one() + + posted_message = super()._process_step(mail_channel) + + if self.step_type == 'create_lead': + self._process_step_create_lead(mail_channel) + + return posted_message + + def _process_step_create_lead(self, mail_channel): + """ When reaching a 'create_lead' step, we extract the relevant information: visitor's + email, phone and conversation history to create a crm.lead. + + We use the email and phone to update the environment partner's information (if not a public + user) if they differ from the current values. + + The whole conversation history will be saved into the lead's description for reference. + This also allows having a question of type 'free_input_multi' to let the visitor explain + his interest / needs before creating the lead. """ + + customer_values = self._chatbot_prepare_customer_values( + mail_channel, create_partner=False, update_partner=True) + if self.env.user._is_public(): + create_values = { + 'email_from': customer_values['email'], + 'phone': customer_values['phone'], + } + else: + partner = self.env.user.partner_id + create_values = { + 'partner_id': partner.id, + 'company_id': partner.company_id.id, + } + + create_values.update(self._chatbot_crm_prepare_lead_values( + mail_channel, customer_values['description'])) + + self.env['crm.lead'].create(create_values) diff --git a/addons/crm_livechat/models/mail_channel.py b/addons/crm_livechat/models/mail_channel.py index 4c5e6ef806f..51d3e905d9a 100644 --- a/addons/crm_livechat/models/mail_channel.py +++ b/addons/crm_livechat/models/mail_channel.py @@ -26,10 +26,6 @@ class MailChannel(models.Model): :param partner: internal user partner (operator) that created the lead; :param key: operator input in chat ('/lead Lead about Product') """ - description = ''.join( - '%s: %s
' % (message.author_id.name or self.anonymous_name, html2plaintext(message.body)) - for message in self.message_ids.sorted('id') - ) # converting message body back to plaintext for correct data formating in description # if public user is part of the chat: consider lead to be linked to an # anonymous user whatever the participants. Otherwise keep only share # partners (no user or portal user) to link to the lead. @@ -47,7 +43,7 @@ class MailChannel(models.Model): 'partner_id': customers[0].id if customers else False, 'user_id': False, 'team_id': False, - 'description': description, + 'description': self._get_channel_history(), 'referred': partner.name, 'source_id': utm_source and utm_source.id, }) diff --git a/addons/crm_livechat/tests/__init__.py b/addons/crm_livechat/tests/__init__.py index 3721104aa97..9219bc543f7 100644 --- a/addons/crm_livechat/tests/__init__.py +++ b/addons/crm_livechat/tests/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import chatbot_common +from . import test_chatbot_lead from . import test_crm_lead diff --git a/addons/crm_livechat/tests/chatbot_common.py b/addons/crm_livechat/tests/chatbot_common.py new file mode 100644 index 00000000000..9fc0ced2cda --- /dev/null +++ b/addons/crm_livechat/tests/chatbot_common.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.im_livechat.tests.chatbot_common import ChatbotCase +from odoo.addons.mail.tests.common import mail_new_test_user + + +class CrmChatbotCase(ChatbotCase): + + @classmethod + def setUpClass(cls): + super(CrmChatbotCase, cls).setUpClass() + + cls.company_id = cls.env['res.company'].create({ + 'name': 'Test Company', + 'country_id': cls.env.ref('base.be').id, + }) + + cls.user_public = mail_new_test_user( + cls.env, login='user_public', groups='base.group_public', name='Public User') + cls.user_portal = mail_new_test_user( + cls.env, login='user_portal', groups='base.group_portal', name='Portal User', + company_id=cls.company_id.id, email='portal@example.com') + # update company_id on partner since the user's company is not propagated + cls.user_portal.partner_id.write({'company_id': cls.company_id.id}) + + cls.sale_team = cls.env['crm.team'].create({ + 'name': 'Test Sale Team 1', + 'company_id': cls.company_id.id, + }) + + cls.sale_team_with_lead = cls.env['crm.team'].create({ + 'name': 'Test Sale Team 2', + 'use_leads': True, + 'company_id': cls.company_id.id, + }) + + cls.step_dispatch_create_lead = cls.env['chatbot.script.answer'].sudo().create({ + 'name': 'Create a lead', + 'script_step_id': cls.step_dispatch.id, + }) + + [ + cls.step_create_lead_email, + cls.step_create_lead_phone, + cls.step_create_lead, + ] = cls.env['chatbot.script.step'].sudo().create([{ + 'step_type': 'question_email', + 'message': 'Could you provide us your email please.', + 'sequence': 20, + 'triggering_answer_ids': [(4, cls.step_dispatch_create_lead.id)], + 'chatbot_script_id': cls.chatbot_script.id, + }, { + 'step_type': 'question_phone', + 'message': 'Could you also provide your phone please.', + 'sequence': 21, + 'triggering_answer_ids': [(4, cls.step_dispatch_create_lead.id)], + 'chatbot_script_id': cls.chatbot_script.id, + }, { + 'step_type': 'create_lead', + 'message': 'Thank you! A lead has been created.', + 'sequence': 22, + 'triggering_answer_ids': [(4, cls.step_dispatch_create_lead.id)], + 'crm_team_id': cls.sale_team.id, + 'chatbot_script_id': cls.chatbot_script.id, + }]) diff --git a/addons/crm_livechat/tests/test_chatbot_lead.py b/addons/crm_livechat/tests/test_chatbot_lead.py new file mode 100644 index 00000000000..35e7a1b3c2a --- /dev/null +++ b/addons/crm_livechat/tests/test_chatbot_lead.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.crm_livechat.tests import chatbot_common +from odoo.tests.common import users + + +class CrmChatbotCase(chatbot_common.CrmChatbotCase): + + @users('user_public') + def test_chatbot_lead_public_user(self): + self._chatbot_create_lead(self.user_public) + + created_lead = self.env['crm.lead'].sudo().search([], limit=1, order='id desc') + self.assertEqual(created_lead.name, "Testing Bot's New Lead") + self.assertEqual(created_lead.email_from, 'test2@example.com') + self.assertEqual(created_lead.phone, '123456') + + self.assertEqual(created_lead.team_id, self.sale_team) + self.assertEqual(created_lead.type, 'opportunity') + + @users('user_portal') + def test_chatbot_lead_portal_user(self): + self.step_create_lead.write({'crm_team_id': self.sale_team_with_lead}) + self._chatbot_create_lead(self.user_portal) + + created_lead = self.env['crm.lead'].sudo().search([], limit=1, order='id desc') + self.assertEqual(created_lead.name, "Testing Bot's New Lead") + self.assertNotEqual(created_lead.email_from, 'test2@example.com', "User's email should'nt have been overridden") + self.assertEqual(created_lead.phone, '123456', "User's phone should have been updated") + + self.assertEqual(created_lead.team_id, self.sale_team_with_lead) + self.assertEqual(created_lead.type, 'lead') + + def _chatbot_create_lead(self, user): + channel_info = self.livechat_channel._open_livechat_mail_channel( + anonymous_name='Test Visitor', chatbot_script=self.chatbot_script, user_id=user.id) + mail_channel = self.env['mail.channel'].sudo().browse(channel_info['id']) + + self._post_answer_and_trigger_next_step( + mail_channel, + self.step_dispatch_create_lead.name, + chatbot_script_answer=self.step_dispatch_create_lead + ) + self.assertEqual(mail_channel.chatbot_current_step_id, self.step_create_lead_email) + self._post_answer_and_trigger_next_step(mail_channel, 'test2@example.com') + + self.assertEqual(mail_channel.chatbot_current_step_id, self.step_create_lead_phone) + self._post_answer_and_trigger_next_step(mail_channel, '123456') + + self.assertEqual(mail_channel.chatbot_current_step_id, self.step_create_lead) diff --git a/addons/crm_livechat/views/chatbot_script_step_views.xml b/addons/crm_livechat/views/chatbot_script_step_views.xml new file mode 100644 index 00000000000..adfebca1eee --- /dev/null +++ b/addons/crm_livechat/views/chatbot_script_step_views.xml @@ -0,0 +1,15 @@ + + + + + chatbot.script.step.view.form.inherit.crm.livechat + chatbot.script.step + + + + + + + + + diff --git a/addons/crm_livechat/views/chatbot_script_views.xml b/addons/crm_livechat/views/chatbot_script_views.xml new file mode 100644 index 00000000000..5f556ac9232 --- /dev/null +++ b/addons/crm_livechat/views/chatbot_script_views.xml @@ -0,0 +1,18 @@ + + + + + chatbot.script.view.form.inherit.crm.livechat + chatbot.script + + +
+ +
+
+
+ +
diff --git a/addons/website_crm_livechat/models/__init__.py b/addons/website_crm_livechat/models/__init__.py index e81172ec3dd..61d3b3b91e7 100644 --- a/addons/website_crm_livechat/models/__init__.py +++ b/addons/website_crm_livechat/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import chatbot_script_step from . import crm_lead from . import mail_channel diff --git a/addons/website_crm_livechat/models/chatbot_script_step.py b/addons/website_crm_livechat/models/chatbot_script_step.py new file mode 100644 index 00000000000..27a23f7d49b --- /dev/null +++ b/addons/website_crm_livechat/models/chatbot_script_step.py @@ -0,0 +1,16 @@ + +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, models + + +class ChatbotScriptStep(models.Model): + _inherit = 'chatbot.script.step' + + def _chatbot_crm_prepare_lead_values(self, mail_channel, description): + values = super()._chatbot_crm_prepare_lead_values(mail_channel, description) + if mail_channel.livechat_visitor_id: + values['name'] = _("%s's New Lead", mail_channel.livechat_visitor_id.display_name) + values['visitor_ids'] = [(4, mail_channel.livechat_visitor_id.id)] + return values