19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, models, fields
from odoo.tools import email_normalize, html2plaintext, is_html_empty, plaintext2html
from odoo import api, Command, models, fields
from odoo.http import request
from odoo.tools import email_normalize, get_lang, html2plaintext, is_html_empty, plaintext2html
from odoo.addons.mail.tools.discuss import Store
from odoo.exceptions import ValidationError
class ChatbotScript(models.Model):
@ -20,21 +23,33 @@ class ChatbotScript(models.Model):
script_step_ids = fields.One2many('chatbot.script.step', 'chatbot_script_id',
copy=True, string='Script Steps')
operator_partner_id = fields.Many2one('res.partner', string='Bot Operator',
ondelete='restrict', required=True, copy=False)
ondelete='restrict', required=True, copy=False, index=True)
livechat_channel_count = fields.Integer(string='Livechat Channel Count', compute='_compute_livechat_channel_count')
first_step_warning = fields.Selection([
('first_step_operator', 'First Step Operator'),
('first_step_invalid', 'First Step Invalid'),
], compute="_compute_first_step_warning")
@api.constrains("script_step_ids")
def _check_question_selection(self):
for step in self.script_step_ids:
if step.step_type == "question_selection" and not step.answer_ids:
raise ValidationError(self.env._("Step of type 'Question' must have answers."))
@api.onchange("script_step_ids")
def _onchange_script_step_ids(self):
for step in self.script_step_ids:
if step.step_type != "question_selection" and step.answer_ids:
step.answer_ids = [Command.clear()]
def _compute_livechat_channel_count(self):
channels_data = self.env['im_livechat.channel.rule'].read_group(
[('chatbot_script_id', 'in', self.ids)], ['channel_id:count_distinct'], ['chatbot_script_id'])
mapped_channels = {channel['chatbot_script_id'][0]: channel['channel_id'] for channel in channels_data}
channels_data = self.env['im_livechat.channel.rule']._read_group(
[('chatbot_script_id', 'in', self.ids)], ['chatbot_script_id'], ['channel_id:count_distinct'])
mapped_channels = {chatbot_script.id: count_distinct for chatbot_script, count_distinct in channels_data}
for script in self:
script.livechat_channel_count = mapped_channels.get(script.id, 0)
@api.depends('script_step_ids.step_type')
@api.depends("script_step_ids.is_forward_operator", "script_step_ids.step_type" )
def _compute_first_step_warning(self):
for script in self:
allowed_first_step_types = [
@ -45,14 +60,17 @@ class ChatbotScript(models.Model):
'free_input_multi',
]
welcome_steps = script.script_step_ids and script._get_welcome_steps()
if welcome_steps and welcome_steps[-1].step_type == 'forward_operator':
if welcome_steps and welcome_steps[-1].is_forward_operator:
script.first_step_warning = 'first_step_operator'
elif welcome_steps and welcome_steps[-1].step_type not in allowed_first_step_types:
script.first_step_warning = 'first_step_invalid'
else:
script.first_step_warning = False
@api.returns('self', lambda value: value.id)
def copy_data(self, default=None):
vals_list = super().copy_data(default=default)
return [dict(vals, title=self.env._("%s (copy)", script.title)) for script, vals in zip(self, vals_list)]
def copy(self, default=None):
""" Correctly copy the 'triggering_answer_ids' field from the original script_step_ids to the clone.
This needs to be done in post-processing to make sure we get references to the newly created
@ -60,35 +78,32 @@ class ChatbotScript(models.Model):
This implementation assumes that the order of created steps and answers will be kept between
the original and the clone, using 'zip()' to match the records between the two. """
default = default or {}
default['title'] = self.title + _(' (copy)')
clone_chatbot_script = super().copy(default=default)
new_scripts = super().copy(default=default)
if 'question_ids' in default:
return clone_chatbot_script
return new_scripts
original_steps = self.script_step_ids.sorted()
clone_steps = clone_chatbot_script.script_step_ids.sorted()
for old_script, new_script in zip(self, new_scripts):
original_steps = old_script.script_step_ids.sorted()
clone_steps = new_script.script_step_ids.sorted()
answers_map = {}
for clone_step, original_step in zip(clone_steps, original_steps):
for clone_answer, original_answer in zip(clone_step.answer_ids.sorted(), original_step.answer_ids.sorted()):
answers_map[original_answer] = clone_answer
answers_map = {}
for clone_step, original_step in zip(clone_steps, original_steps):
for clone_answer, original_answer in zip(clone_step.answer_ids.sorted(), original_step.answer_ids.sorted()):
answers_map[original_answer] = clone_answer
for clone_step, original_step in zip(clone_steps, original_steps):
clone_step.write({
'triggering_answer_ids': [
(4, answer.id)
for answer in [
answers_map[original_answer]
for original_answer
in original_step.triggering_answer_ids
for clone_step, original_step in zip(clone_steps, original_steps):
clone_step.write({
'triggering_answer_ids': [
(4, answer.id)
for answer in [
answers_map[original_answer]
for original_answer
in original_step.triggering_answer_ids
]
]
]
})
return clone_chatbot_script
})
return new_scripts
@api.model_create_multi
def create(self, vals_list):
@ -137,7 +152,7 @@ class ChatbotScript(models.Model):
end user.
This is important because we need to display those welcoming steps in a special fashion on
the frontend, since those are not inserted into the mail.channel as actual mail.messages,
the frontend, since those are not inserted into the discuss.channel as actual mail.messages,
to avoid bloating the channels with bot messages if the end-user never interacts with it. """
self.ensure_one()
@ -149,23 +164,23 @@ class ChatbotScript(models.Model):
return welcome_steps
def _post_welcome_steps(self, mail_channel):
def _post_welcome_steps(self, discuss_channel):
""" Welcome messages are only posted after the visitor's first interaction with the chatbot.
See 'chatbot.script#_get_welcome_steps()' for more details.
Side note: it is important to set the 'chatbot_current_step_id' on each iteration so that
it's correctly set when going into 'mail_channel#_message_post_after_hook()'. """
it's correctly set when going into 'discuss_channel#_message_post_after_hook()'. """
self.ensure_one()
posted_messages = self.env['mail.message']
for welcome_step in self._get_welcome_steps():
mail_channel.chatbot_current_step_id = welcome_step.id
discuss_channel.chatbot_current_step_id = welcome_step.id
if not is_html_empty(welcome_step.message):
posted_messages += mail_channel.with_context(mail_create_nosubscribe=True).message_post(
posted_messages += discuss_channel.with_context(mail_post_autofollow_author_skip=True).message_post(
author_id=self.operator_partner_id.id,
body=plaintext2html(welcome_step.message),
body=plaintext2html(welcome_step.message, with_paragraph=False),
message_type='comment',
subtype_xmlid='mail.mt_comment',
)
@ -182,35 +197,29 @@ class ChatbotScript(models.Model):
# Tooling / Misc
# --------------------------
def _format_for_frontend(self):
""" Small utility method that formats the script into a dict usable by the frontend code. """
self.ensure_one()
def _to_store_defaults(self, target):
return [Store.One("operator_partner_id", ["name"]), "title"]
return {
'chatbot_script_id': self.id,
'chatbot_name': self.title,
'chatbot_operator_partner_id': self.operator_partner_id.id,
'chatbot_welcome_steps': [
step._format_for_frontend()
for step in self._get_welcome_steps()
]
}
def _validate_email(self, email_address, mail_channel):
def _validate_email(self, email_address, discuss_channel):
email_address = html2plaintext(email_address)
email_normalized = email_normalize(email_address)
posted_message = False
error_message = False
if not email_normalized:
error_message = _(
error_message = self.env._(
"'%(input_email)s' does not look like a valid email. Can you please try again?",
input_email=email_address
)
posted_message = mail_channel._chatbot_post_message(self, plaintext2html(error_message))
posted_message = discuss_channel._chatbot_post_message(self, plaintext2html(error_message))
return {
'success': bool(email_normalized),
'posted_message': posted_message,
'error_message': error_message,
}
def _get_chatbot_language(self):
return get_lang(
self.env, lang_code=request and request.httprequest.cookies.get("frontend_lang")
).code