mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 12:42:04 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, models, fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Command
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import html2plaintext, is_html_empty, email_normalize, plaintext2html
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tools import html2plaintext, email_normalize
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
from collections import defaultdict
|
||||
from markupsafe import Markup
|
||||
|
|
@ -15,12 +14,12 @@ class ChatbotScriptStep(models.Model):
|
|||
_name = 'chatbot.script.step'
|
||||
_description = 'Chatbot Script Step'
|
||||
_order = 'sequence, id'
|
||||
_rec_name = 'message'
|
||||
|
||||
message = fields.Text(string='Message', translate=True)
|
||||
name = fields.Char(string="Name", compute="_compute_name")
|
||||
message = fields.Html(string="Message", translate=True)
|
||||
sequence = fields.Integer(string='Sequence')
|
||||
chatbot_script_id = fields.Many2one(
|
||||
'chatbot.script', string='Chatbot', required=True, ondelete='cascade')
|
||||
'chatbot.script', string='Chatbot', required=True, index=True, ondelete='cascade')
|
||||
step_type = fields.Selection([
|
||||
('text', 'Text'),
|
||||
('question_selection', 'Question'),
|
||||
|
|
@ -35,12 +34,28 @@ class ChatbotScriptStep(models.Model):
|
|||
'chatbot.script.answer', 'script_step_id',
|
||||
copy=True, string='Answers')
|
||||
triggering_answer_ids = fields.Many2many(
|
||||
'chatbot.script.answer', domain="[('script_step_id.sequence', '<', sequence)]",
|
||||
'chatbot.script.answer', domain="[('script_step_id.sequence', '<', sequence), ('script_step_id.chatbot_script_id', '=', chatbot_script_id)]",
|
||||
compute='_compute_triggering_answer_ids', readonly=False, store=True,
|
||||
copy=False, # copied manually, see chatbot.script#copy
|
||||
string='Only If', help='Show this step only if all of these answers have been selected.')
|
||||
# forward-operator specifics
|
||||
is_forward_operator = fields.Boolean(compute="_compute_is_forward_operator")
|
||||
is_forward_operator_child = fields.Boolean(compute='_compute_is_forward_operator_child')
|
||||
operator_expertise_ids = fields.Many2many(
|
||||
"im_livechat.expertise",
|
||||
string="Operator Expertise",
|
||||
help="When forwarding live chat conversations, the chatbot will prioritize users with matching expertise.",
|
||||
)
|
||||
|
||||
@api.depends("sequence", "chatbot_script_id")
|
||||
@api.depends_context('lang')
|
||||
def _compute_name(self):
|
||||
for step in self:
|
||||
step.name = self.env._(
|
||||
"%(title)s - Step %(sequence)d",
|
||||
title=step.chatbot_script_id.title,
|
||||
sequence=step.sequence,
|
||||
)
|
||||
|
||||
@api.depends('sequence')
|
||||
def _compute_triggering_answer_ids(self):
|
||||
|
|
@ -50,13 +65,25 @@ class ChatbotScriptStep(models.Model):
|
|||
if update_command:
|
||||
step.triggering_answer_ids = update_command
|
||||
|
||||
@api.depends('sequence', 'triggering_answer_ids', 'chatbot_script_id.script_step_ids.triggering_answer_ids',
|
||||
'chatbot_script_id.script_step_ids.answer_ids', 'chatbot_script_id.script_step_ids.sequence')
|
||||
@api.depends("step_type")
|
||||
def _compute_is_forward_operator(self):
|
||||
for step in self:
|
||||
step.is_forward_operator = step.step_type == "forward_operator"
|
||||
|
||||
@api.depends(
|
||||
"chatbot_script_id.script_step_ids.answer_ids",
|
||||
"chatbot_script_id.script_step_ids.is_forward_operator",
|
||||
"chatbot_script_id.script_step_ids.sequence",
|
||||
"chatbot_script_id.script_step_ids.step_type",
|
||||
"chatbot_script_id.script_step_ids.triggering_answer_ids",
|
||||
"sequence",
|
||||
"triggering_answer_ids",
|
||||
)
|
||||
def _compute_is_forward_operator_child(self):
|
||||
parent_steps_by_chatbot = {}
|
||||
for chatbot in self.chatbot_script_id:
|
||||
parent_steps_by_chatbot[chatbot.id] = chatbot.script_step_ids.filtered(
|
||||
lambda step: step.step_type in ['forward_operator', 'question_selection']
|
||||
lambda step: step.is_forward_operator or step.step_type == "question_selection"
|
||||
).sorted(lambda s: s.sequence, reverse=True)
|
||||
for step in self:
|
||||
parent_steps = parent_steps_by_chatbot[step.chatbot_script_id.id].filtered(
|
||||
|
|
@ -65,9 +92,9 @@ class ChatbotScriptStep(models.Model):
|
|||
parent = step
|
||||
while True:
|
||||
parent = parent._get_parent_step(parent_steps)
|
||||
if not parent or parent.step_type == 'forward_operator':
|
||||
if not parent or parent.is_forward_operator:
|
||||
break
|
||||
step.is_forward_operator_child = parent and parent.step_type == 'forward_operator'
|
||||
step.is_forward_operator_child = parent and parent.is_forward_operator
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
@ -92,18 +119,15 @@ class ChatbotScriptStep(models.Model):
|
|||
step_values.append(vals)
|
||||
vals_by_chatbot_id[chatbot_id] = step_values
|
||||
|
||||
max_sequence_by_chatbot = {}
|
||||
if vals_by_chatbot_id:
|
||||
read_group_results = self.env['chatbot.script.step'].read_group(
|
||||
[('chatbot_script_id', 'in', list(vals_by_chatbot_id.keys()))],
|
||||
['sequence:max'],
|
||||
['chatbot_script_id']
|
||||
)
|
||||
|
||||
max_sequence_by_chatbot = {
|
||||
read_group_result['chatbot_script_id'][0]: read_group_result['sequence']
|
||||
for read_group_result in read_group_results
|
||||
}
|
||||
read_group_results = self.env['chatbot.script.step']._read_group(
|
||||
[('chatbot_script_id', 'in', list(vals_by_chatbot_id))],
|
||||
['chatbot_script_id'],
|
||||
['sequence:max'],
|
||||
)
|
||||
max_sequence_by_chatbot = {
|
||||
chatbot_script.id: sequence
|
||||
for chatbot_script, sequence in read_group_results
|
||||
}
|
||||
|
||||
for chatbot_id, step_vals in vals_by_chatbot_id.items():
|
||||
current_sequence = 0
|
||||
|
|
@ -123,33 +147,33 @@ class ChatbotScriptStep(models.Model):
|
|||
# Business Methods
|
||||
# --------------------------
|
||||
|
||||
def _chatbot_prepare_customer_values(self, mail_channel, create_partner=True, update_partner=True):
|
||||
""" Common method that allows retreiving default customer values from the mail.channel
|
||||
def _chatbot_prepare_customer_values(self, discuss_channel, create_partner=True, update_partner=True):
|
||||
""" Common method that allows retreiving default customer values from the discuss.channel
|
||||
following a chatbot.script.
|
||||
|
||||
This method will return a dict containing the 'customer' values such as:
|
||||
{
|
||||
'partner': The created partner (see 'create_partner') or the partner from the
|
||||
environment if not public
|
||||
'email': The email extracted from the mail.channel messages
|
||||
'email': The email extracted from the discuss.channel messages
|
||||
(see step_type 'question_email')
|
||||
'phone': The phone extracted from the mail.channel messages
|
||||
'phone': The phone extracted from the discuss.channel messages
|
||||
(see step_type 'question_phone')
|
||||
'description': A default description containing the "Please contact me on" and "Please
|
||||
call me on" with the related email and phone numbers.
|
||||
Can be used as a default description to create leads or tickets for example.
|
||||
}
|
||||
|
||||
:param record mail_channel: the mail.channel holding the visitor's conversation with the bot.
|
||||
:param record discuss_channel: the discuss.channel holding the visitor's conversation with the bot.
|
||||
:param bool create_partner: whether or not to create a res.partner is the current user is public.
|
||||
Defaults to True.
|
||||
:param bool update_partner: whether or not to set update the email and phone on the res.partner
|
||||
from the environment (if not a public user) if those are not set yet. Defaults to True.
|
||||
|
||||
:return dict: a dict containing the customer values."""
|
||||
:returns: a dict containing the customer values."""
|
||||
|
||||
partner = False
|
||||
user_inputs = mail_channel._chatbot_find_customer_values_in_messages({
|
||||
user_inputs = discuss_channel._chatbot_find_customer_values_in_messages({
|
||||
'question_email': 'email',
|
||||
'question_phone': 'phone',
|
||||
})
|
||||
|
|
@ -176,9 +200,9 @@ class ChatbotScriptStep(models.Model):
|
|||
|
||||
description = Markup('')
|
||||
if input_email:
|
||||
description += Markup('%s<strong>%s</strong><br>') % (_('Please contact me on: '), input_email)
|
||||
description += Markup("%s<strong>%s</strong><br>") % (_("Email: "), input_email)
|
||||
if input_phone:
|
||||
description += Markup('%s<strong>%s</strong><br>') % (_('Please call me on: '), input_phone)
|
||||
description += Markup("%s<strong>%s</strong><br>") % (_("Phone: "), input_phone)
|
||||
if description:
|
||||
description += Markup('<br>')
|
||||
|
||||
|
|
@ -189,6 +213,17 @@ class ChatbotScriptStep(models.Model):
|
|||
'description': description,
|
||||
}
|
||||
|
||||
def _find_first_user_free_input(self, discuss_channel):
|
||||
"""Find the first message from the visitor responding to a free_input step."""
|
||||
chatbot_partner = self.chatbot_script_id.operator_partner_id
|
||||
user_answers = discuss_channel.chatbot_message_ids.filtered(
|
||||
lambda m: m.mail_message_id.author_id != chatbot_partner
|
||||
).sorted("id")
|
||||
for answer in user_answers:
|
||||
if answer.script_step_id.step_type in ("free_input_single", "free_input_multi"):
|
||||
return answer.mail_message_id
|
||||
return self.env["mail.message"]
|
||||
|
||||
def _fetch_next_step(self, selected_answer_ids):
|
||||
""" Fetch the next step depending on the user's selected answers.
|
||||
If a step contains multiple triggering answers from the same step the condition between
|
||||
|
|
@ -219,12 +254,9 @@ class ChatbotScriptStep(models.Model):
|
|||
-> NOK
|
||||
"""
|
||||
self.ensure_one()
|
||||
domain = [('chatbot_script_id', '=', self.chatbot_script_id.id), ('sequence', '>', self.sequence)]
|
||||
domain = Domain('chatbot_script_id', '=', self.chatbot_script_id.id) & Domain('sequence', '>', self.sequence)
|
||||
if selected_answer_ids:
|
||||
domain = expression.AND([domain, [
|
||||
'|',
|
||||
('triggering_answer_ids', '=', False),
|
||||
('triggering_answer_ids', 'in', selected_answer_ids.ids)]])
|
||||
domain &= Domain('triggering_answer_ids', 'in', selected_answer_ids.ids + [False])
|
||||
steps = self.env['chatbot.script.step'].search(domain)
|
||||
for step in steps:
|
||||
if not step.triggering_answer_ids:
|
||||
|
|
@ -257,18 +289,20 @@ class ChatbotScriptStep(models.Model):
|
|||
return step
|
||||
return self.env['chatbot.script.step']
|
||||
|
||||
def _is_last_step(self, mail_channel=False):
|
||||
def _is_last_step(self, discuss_channel=False):
|
||||
self.ensure_one()
|
||||
mail_channel = mail_channel or self.env['mail.channel']
|
||||
discuss_channel = discuss_channel or self.env['discuss.channel']
|
||||
|
||||
# if it's not a question and if there is no next step, then we end the script
|
||||
if self.step_type != 'question_selection' and not self._fetch_next_step(
|
||||
mail_channel.chatbot_message_ids.user_script_answer_id):
|
||||
# sudo: chatbot.script.answser - visitor can access their own answers
|
||||
if self.step_type != "question_selection" and not self._fetch_next_step(
|
||||
discuss_channel.sudo().chatbot_message_ids.user_script_answer_id
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _process_answer(self, mail_channel, message_body):
|
||||
def _process_answer(self, discuss_channel, message_body):
|
||||
""" Method called when the user reacts to the current chatbot.script step.
|
||||
For most chatbot.script.step#step_types it simply returns the next chatbot.script.step of
|
||||
the script (see '_fetch_next_step').
|
||||
|
|
@ -277,7 +311,7 @@ class ChatbotScriptStep(models.Model):
|
|||
we store the user raw answer (the mail message HTML body) into the chatbot.message in order
|
||||
to be able to recover it later (see '_chatbot_prepare_customer_values').
|
||||
|
||||
:param mail_channel:
|
||||
:param discuss_channel:
|
||||
:param message_body:
|
||||
:return: script step to display next
|
||||
:rtype: 'chatbot.script.step' """
|
||||
|
|
@ -289,9 +323,14 @@ class ChatbotScriptStep(models.Model):
|
|||
# if this error is raised, display an error message but do not go to next step
|
||||
raise ValidationError(_('"%s" is not a valid email.', user_text_answer))
|
||||
|
||||
if self.step_type in ['question_email', 'question_phone']:
|
||||
if self.step_type in [
|
||||
"question_email",
|
||||
"question_phone",
|
||||
"free_input_single",
|
||||
"free_input_multi",
|
||||
]:
|
||||
chatbot_message = self.env['chatbot.message'].search([
|
||||
('mail_channel_id', '=', mail_channel.id),
|
||||
('discuss_channel_id', '=', discuss_channel.id),
|
||||
('script_step_id', '=', self.id),
|
||||
], limit=1)
|
||||
|
||||
|
|
@ -299,9 +338,10 @@ class ChatbotScriptStep(models.Model):
|
|||
chatbot_message.write({'user_raw_answer': message_body})
|
||||
self.env.flush_all()
|
||||
|
||||
return self._fetch_next_step(mail_channel.chatbot_message_ids.user_script_answer_id)
|
||||
# sudo: chatbot.script.answer - visitor can access their own answer
|
||||
return self._fetch_next_step(discuss_channel.sudo().chatbot_message_ids.user_script_answer_id)
|
||||
|
||||
def _process_step(self, mail_channel):
|
||||
def _process_step(self, discuss_channel):
|
||||
""" When we reach a chatbot.step in the script we need to do some processing on behalf of
|
||||
the bot. Which is for most chatbot.script.step#step_types just posting the message field.
|
||||
|
||||
|
|
@ -310,74 +350,15 @@ class ChatbotScriptStep(models.Model):
|
|||
Those will have a dedicated processing method with specific docstrings.
|
||||
|
||||
Returns the mail.message posted by the chatbot's operator_partner_id. """
|
||||
|
||||
self.ensure_one()
|
||||
# We change the current step to the new step
|
||||
mail_channel.chatbot_current_step_id = self.id
|
||||
|
||||
if self.step_type == 'forward_operator':
|
||||
return self._process_step_forward_operator(mail_channel)
|
||||
return discuss_channel._forward_human_operator(chatbot_script_step=self)
|
||||
return discuss_channel._chatbot_post_message(self.chatbot_script_id, self.message)
|
||||
|
||||
return mail_channel._chatbot_post_message(self.chatbot_script_id, plaintext2html(self.message))
|
||||
|
||||
def _process_step_forward_operator(self, mail_channel):
|
||||
""" Special type of step that will add a human operator to the conversation when reached,
|
||||
which stops the script and allow the visitor to discuss with a real person.
|
||||
|
||||
In case we don't find any operator (e.g: no-one is available) we don't post any messages.
|
||||
The script will continue normally, which allows to add extra steps when it's the case
|
||||
(e.g: ask for the visitor's email and create a lead). """
|
||||
|
||||
human_operator = False
|
||||
posted_message = False
|
||||
|
||||
if mail_channel.livechat_channel_id:
|
||||
human_operator = mail_channel.livechat_channel_id._get_random_operator()
|
||||
|
||||
# handle edge case where we found yourself as available operator -> don't do anything
|
||||
# it will act as if no-one is available (which is fine)
|
||||
if human_operator and human_operator != self.env.user:
|
||||
mail_channel.sudo().add_members(
|
||||
human_operator.partner_id.ids,
|
||||
open_chat_window=True,
|
||||
post_joined_message=False)
|
||||
|
||||
# rename the channel to include the operator's name
|
||||
mail_channel.sudo().name = ' '.join([
|
||||
self.env.user.display_name if not self.env.user._is_public() else mail_channel.anonymous_name,
|
||||
human_operator.livechat_username if human_operator.livechat_username else human_operator.name
|
||||
])
|
||||
|
||||
if self.message:
|
||||
# first post the message of the step (if we have one)
|
||||
posted_message = mail_channel._chatbot_post_message(self.chatbot_script_id, plaintext2html(self.message))
|
||||
|
||||
# then post a small custom 'Operator has joined' notification
|
||||
mail_channel._chatbot_post_message(
|
||||
self.chatbot_script_id,
|
||||
Markup('<div class="o_mail_notification">%s</div>') % _('%s has joined', human_operator.partner_id.name))
|
||||
|
||||
mail_channel._broadcast(human_operator.partner_id.ids)
|
||||
mail_channel.channel_pin(pinned=True)
|
||||
|
||||
return posted_message
|
||||
|
||||
# --------------------------
|
||||
# Tooling / Misc
|
||||
# --------------------------
|
||||
|
||||
def _format_for_frontend(self):
|
||||
""" Small utility method that formats the step into a dict usable by the frontend code. """
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'chatbot_script_step_id': self.id,
|
||||
'chatbot_step_answers': [{
|
||||
'id': answer.id,
|
||||
'label': answer.name,
|
||||
'redirect_link': answer.redirect_link,
|
||||
} for answer in self.answer_ids],
|
||||
'chatbot_step_message': plaintext2html(self.message) if not is_html_empty(self.message) else False,
|
||||
'chatbot_step_is_last': self._is_last_step(),
|
||||
'chatbot_step_type': self.step_type
|
||||
}
|
||||
def _to_store_defaults(self, target):
|
||||
return [
|
||||
Store.Many("answer_ids"),
|
||||
Store.Attr("is_last", lambda step: step._is_last_step()),
|
||||
"message",
|
||||
"step_type",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue