Files
enterprise-addons/whatsapp/wizard/whatsapp_composer.py
T

331 lines
17 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import logging
from ast import literal_eval
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, RedirectWarning, UserError
from odoo.addons.whatsapp.tools import phone_validation as wa_phone_validation
_logger = logging.getLogger(__name__)
class WhatsappComposer(models.TransientModel):
_name = 'whatsapp.composer'
_description = 'Send WhatsApp Wizard'
def _raise_no_template_error(self, res_model):
if self.env.user.has_group('whatsapp.group_whatsapp_admin'):
raise RedirectWarning(
_("No approved WhatsApp Templates are available for this model."),
self.env.ref('whatsapp.whatsapp_template_action').id,
_("Configure Templates"),
{'search_default_model': res_model}
)
else:
raise ValidationError(_("No approved WhatsApp Templates are available for this model."))
@api.model
def default_get(self, fields):
result = super().default_get(fields)
context = self.env.context
if context.get('active_model'):
result['res_model'] = context['active_model']
wa_template_id = self.env['whatsapp.template']._find_default_for_model(result['res_model'])
if wa_template_id and not result.get('wa_template_id'):
result['wa_template_id'] = wa_template_id.id
elif not wa_template_id and not result.get('wa_template_id'):
self._raise_no_template_error(result['res_model'])
if context.get('active_ids') or context.get('active_id'):
result['res_ids'] = context.get('active_ids') or [context.get('active_id')]
if context.get('active_ids') and len(context['active_ids']) > 1:
result['batch_mode'] = True
return result
# documents
attachment_id = fields.Many2one('ir.attachment', index=True)
res_ids = fields.Char('Document IDs', required=True)
res_model = fields.Char('Document Model Name', required=True)
batch_mode = fields.Boolean("Is Multiple Records")
# content
phone = fields.Char(string="Phone", compute="_compute_number", readonly=False, store=True)
invalid_phone_number_count = fields.Integer(compute="_compute_invalid_phone_number_count")
wa_template_id = fields.Many2one(comodel_name="whatsapp.template", string="Template")
preview_whatsapp = fields.Html(compute="_compute_preview_whatsapp", string="Message Preview")
# free texts
number_of_free_text = fields.Integer(string="Number of free text", compute='_compute_number_of_free_text')
number_of_free_text_button = fields.Integer(string="Number of free text Buttons", compute='_compute_number_of_free_text_button')
is_header_free_text = fields.Boolean(compute='_compute_is_header_free_text')
is_button_dynamic = fields.Boolean(compute='_compute_is_button_dynamic')
header_text_1 = fields.Char(string="Header Free Text", compute='_compute_free_text', readonly=False, store=True)
free_text_1 = fields.Char(string="Free Text 1", compute='_compute_free_text', readonly=False, store=True)
free_text_2 = fields.Char(string="Free Text 2", compute='_compute_free_text', readonly=False, store=True)
free_text_3 = fields.Char(string="Free Text 3", compute='_compute_free_text', readonly=False, store=True)
free_text_4 = fields.Char(string="Free Text 4", compute='_compute_free_text', readonly=False, store=True)
free_text_5 = fields.Char(string="Free Text 5", compute='_compute_free_text', readonly=False, store=True)
free_text_6 = fields.Char(string="Free Text 6", compute='_compute_free_text', readonly=False, store=True)
free_text_7 = fields.Char(string="Free Text 7", compute='_compute_free_text', readonly=False, store=True)
free_text_8 = fields.Char(string="Free Text 8", compute='_compute_free_text', readonly=False, store=True)
free_text_9 = fields.Char(string="Free Text 9", compute='_compute_free_text', readonly=False, store=True)
free_text_10 = fields.Char(string="Free Text 10", compute='_compute_free_text', readonly=False, store=True)
button_dynamic_url_1 = fields.Char(string="Button Url 1", compute='_compute_button_dynamic_url', readonly=False, store=True)
button_dynamic_url_2 = fields.Char(string="Button Url 2", compute='_compute_button_dynamic_url', readonly=False, store=True)
# ------------------------------------------------------------
# COMPUTES
# ------------------------------------------------------------
@api.depends('wa_template_id')
@api.depends_context('default_phone')
def _compute_number(self):
""" In single mode, 'phone' is the number to contact (can be set through
context, for example when forced through UI). In multi mode it is more
an informational field, holding the first record found numbers. """
for composer in self:
records = self.env[composer.res_model].browse(literal_eval(composer.res_ids))
numbers = []
for record in records[:12]:
if composer.wa_template_id.phone_field:
try:
numbers.append(record._find_value_from_field_path(composer.wa_template_id.phone_field))
except UserError as err:
error_msg = _("Template %(template_name)s holds a wrong configuration for 'phone field'\n%(error_msg)s",
template_name=composer.wa_template_id.name,
error_msg=err.args[0]
)
raise ValidationError(error_msg) from err
if not composer.batch_mode:
phone = self.env.context.get('default_phone')
if not phone:
phone = numbers[0] if numbers and numbers[0] else composer.phone
elif not numbers:
phone = False
else:
other_count = len(records) - len(numbers)
phone = ', '.join(self._extract_digits(num) for num in numbers if num)
if other_count:
phone += _(", ... (%s Others)", other_count)
composer.phone = phone
@api.depends('phone', 'batch_mode')
def _compute_invalid_phone_number_count(self):
for composer in self:
records = self._get_active_records()
if composer.batch_mode:
invalid_phone_number_count = 0
for rec in records:
mobile_number = rec._whatsapp_phone_format(fpath=composer.wa_template_id.phone_field)
if not mobile_number:
invalid_phone_number_count += 1
elif composer.phone:
sanitize_number = records._whatsapp_phone_format(number=composer.phone)
invalid_phone_number_count = 1 if not sanitize_number else 0
else:
invalid_phone_number_count = 1
composer.invalid_phone_number_count = invalid_phone_number_count
@api.depends(lambda self: self._get_free_text_fields())
def _compute_preview_whatsapp(self):
"""This method is used to compute the preview of the whatsapp message."""
for record in self:
rec = record._get_active_records()
if record.wa_template_id and rec:
record.preview_whatsapp = self.env['ir.qweb']._render('whatsapp.template_message_preview', {
'body': record._get_html_preview_whatsapp(rec=rec[0]),
'buttons': record.wa_template_id.button_ids,
'header_type': record.wa_template_id.header_type,
'footer_text': record.wa_template_id.footer_text,
'language_direction': 'rtl' if record.wa_template_id.lang_code in ('ar', 'he', 'fa', 'ur') else 'ltr',
})
else:
record.preview_whatsapp = None
@api.depends('wa_template_id')
def _compute_number_of_free_text_button(self):
for rec in self:
tmpl_vars = rec.wa_template_id.variable_ids
rec.number_of_free_text_button = len(tmpl_vars.filtered(lambda var: var.field_type == 'free_text' and var.line_type == 'button'))
@api.depends('wa_template_id')
def _compute_number_of_free_text(self):
for rec in self:
if rec.wa_template_id:
rec.number_of_free_text = len(rec.wa_template_id.variable_ids.filtered(lambda line: line.field_type == 'free_text' and line.line_type == 'body'))
else:
rec.number_of_free_text = 0
@api.depends('wa_template_id')
def _compute_is_header_free_text(self):
for rec in self:
if rec.wa_template_id and rec.wa_template_id.variable_ids and rec.wa_template_id.variable_ids.filtered(lambda line: line.field_type == 'free_text' and line.line_type == 'header'):
rec.is_header_free_text = True
else:
rec.is_header_free_text = False
@api.depends('wa_template_id')
def _compute_is_button_dynamic(self):
for rec in self:
if rec.wa_template_id and rec.wa_template_id.variable_ids and rec.wa_template_id.variable_ids.filtered(lambda line: line.field_type == 'free_text' and line.line_type == 'button'):
rec.is_button_dynamic = True
else:
rec.is_button_dynamic = False
@api.depends('wa_template_id')
def _compute_button_dynamic_url(self):
for rec in self:
freetext_btn_vars = rec.wa_template_id.variable_ids.filtered(lambda line: line.line_type == 'button' and line.field_type == 'free_text')
freetext_btn_vars = freetext_btn_vars.sorted(key=lambda var: var.button_id.sequence)
if len(freetext_btn_vars) > 0:
rec.button_dynamic_url_1 = freetext_btn_vars[0].demo_value
if len(freetext_btn_vars) > 1:
rec.button_dynamic_url_2 = freetext_btn_vars[1].demo_value
@api.depends('wa_template_id')
def _compute_free_text(self):
for rec in self:
if rec.wa_template_id.header_type == 'text':
header_param = rec.wa_template_id.variable_ids.filtered(lambda line: line.line_type == 'header' and line.field_type == 'free_text')
if rec.wa_template_id.variable_ids and header_param:
rec.header_text_1 = header_param[0].demo_value
if rec.wa_template_id.variable_ids:
free_text_count = 1
filtered_variables = rec.wa_template_id.variable_ids.filtered(lambda line: line.line_type == 'body' and line.field_type == 'free_text')
sorted_variables = filtered_variables.sorted(key=lambda var: var._extract_variable_index())
for param in sorted_variables:
rec[f"free_text_{free_text_count}"] = param.demo_value
free_text_count += 1
def _extract_digits(self, string):
if not string:
return string
matches = re.findall(r"\d+", string)
result = "".join(matches)
return result
def _get_free_text_fields(self):
return ["wa_template_id", "header_text_1", "button_dynamic_url_1", "button_dynamic_url_2"] + [f"free_text_{i}" for i in range(1, 11)]
# ------------------------------------------------------------
# SEND MESSAGES
# ------------------------------------------------------------
def action_send_whatsapp_template(self):
self.ensure_one()
if not self.wa_template_id:
raise ValidationError(_("Please select a WhatsApp Template to send."))
return self._send_whatsapp_template()
def _create_whatsapp_messages(self, force_create=False):
records = self._get_active_records()
if self.wa_template_id and self.wa_template_id.variable_ids:
field_types = self.wa_template_id.variable_ids.mapped('field_type')
if 'user_phone' in field_types and not self.env.user.phone:
raise ValidationError(
_("User phone number required in template but no value set on user profile.")
)
free_text_json = self._get_text_free_json()
message_vals_all = []
raise_exception = not (self.batch_mode or force_create)
for rec in records:
mobile_number = None
if self.batch_mode:
formatted_number_wa = rec._whatsapp_phone_format(
fpath=self.wa_template_id.phone_field,
raise_on_format_error=raise_exception,
)
mobile_number = rec.mapped(self.wa_template_id.phone_field)[0]
elif not mobile_number:
mobile_number = self.phone
formatted_number_wa = rec._whatsapp_phone_format(
number=mobile_number, raise_on_format_error=raise_exception,
)
message_vals = {}
if not formatted_number_wa:
message_vals.update({
'failure_type': 'phone_invalid',
'state': 'error',
})
body = self._get_html_preview_whatsapp(rec=rec)
post_values = {
'attachment_ids': [self.attachment_id.id] if self.attachment_id else [],
'body': body,
'message_type': 'whatsapp_message',
'partner_ids': rec._mail_get_partners()[rec.id].ids or rec._whatsapp_get_responsible().partner_id.ids,
}
if self.res_model == 'discuss.channel':
message = rec.message_post(body=body, message_type="comment", subtype_xmlid='mail.mt_comment')
elif hasattr(records, '_message_log'):
message = rec._message_log(**post_values)
else:
message = self.env['mail.message'].create(
dict(post_values, res_id=rec.id, model=self.res_model,
subtype_id=self.env['ir.model.data']._xmlid_to_res_id("mail.mt_note"))
)
message_vals_all.append(message_vals | {
'mail_message_id': message.id,
'mobile_number': mobile_number,
'mobile_number_formatted': formatted_number_wa,
'free_text_json': free_text_json,
'wa_template_id': self.wa_template_id.id,
'wa_account_id': self.wa_template_id.wa_account_id.id,
})
if message_vals_all:
messages = self.env['whatsapp.message'].create(message_vals_all)
return messages
return self.env["whatsapp.message"]
def _send_whatsapp_template(self, force_send_by_cron=False):
messages = self._create_whatsapp_messages(force_create=force_send_by_cron)
if messages:
messages._send(force_send_by_cron=force_send_by_cron)
return messages
return self.env["whatsapp.message"]
def _get_text_free_json(self):
"""This method is used to prepare free text json using values set in free text field of composer."""
self.ensure_one()
json_vals = {}
if self.header_text_1:
json_vals['header_text'] = self.header_text_1
if self.number_of_free_text:
free_text_field = [f"free_text_{i + 1}" for i in range(self.number_of_free_text)]
for value in free_text_field:
if self[value]:
json_vals[value] = self[value]
if self.button_dynamic_url_1:
json_vals['button_dynamic_url_1'] = self.button_dynamic_url_1
if self.button_dynamic_url_2:
json_vals['button_dynamic_url_2'] = self.button_dynamic_url_2
return json_vals
def _get_html_preview_whatsapp(self, rec):
"""This method is used to get the html preview of the whatsapp message."""
self.ensure_one()
template_variables_value = self.wa_template_id.variable_ids._get_variables_value(rec)
text_vars = self.wa_template_id.variable_ids.filtered(lambda var: var.field_type == 'free_text')
body_text_vars = text_vars.filtered(lambda var: var.line_type == 'body').sorted(key=lambda var: var._extract_variable_index())
for var_index, body_text_var in zip(range(1, self.number_of_free_text + 1), body_text_vars):
free_text_x = self[f'free_text_{var_index}']
if free_text_x:
template_variables_value[f'body-{body_text_var.name}'] = free_text_x
if self.header_text_1 and text_vars.filtered(lambda var: var.line_type == 'header'):
template_variables_value['header-{{1}}'] = self.header_text_1
return self.wa_template_id._get_formatted_body(variable_values=template_variables_value)
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _get_active_records(self):
self.ensure_one()
return self.env[self.res_model].browse(literal_eval(self.res_ids))