diff --git a/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py b/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py index b0852f04c5..b8d021bc4b 100644 --- a/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py +++ b/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py @@ -293,6 +293,7 @@ def _send_payload( "template": { "name": whatsapp_template.template_name, "language": {"code": whatsapp_template.language}, + "components": whatsapp_template.prepare_value_to_send(), }, } ) diff --git a/mail_gateway_whatsapp/models/mail_whatsapp_template.py b/mail_gateway_whatsapp/models/mail_whatsapp_template.py index 6974040f54..4977f76389 100644 --- a/mail_gateway_whatsapp/models/mail_whatsapp_template.py +++ b/mail_gateway_whatsapp/models/mail_whatsapp_template.py @@ -1,15 +1,18 @@ # Copyright 2024 Tecnativa - Carlos López # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import re +from urllib.parse import urlparse import requests from werkzeug.urls import url_join -from odoo import api, fields, models -from odoo.exceptions import UserError +from odoo import Command, _, api, fields, models +from odoo.exceptions import UserError, ValidationError from odoo.tools import ustr -from ..tools.const import supported_languages +from odoo.addons.phone_validation.tools import phone_validation + +from ..tools.const import REG_VARIABLE, supported_languages from .mail_gateway import BASE_URL @@ -61,6 +64,26 @@ class MailWhatsAppTemplate(models.Model): company_id = fields.Many2one( "res.company", related="gateway_id.company_id", store=True ) + model_id = fields.Many2one( + string="Applies to", + comodel_name="ir.model", + default=lambda self: self.env["ir.model"]._get_id("res.partner"), + required=True, + ondelete="cascade", + ) + model = fields.Char(string="Related model", related="model_id.model", store=True) + variable_ids = fields.One2many( + "mail.whatsapp.template.variable", + "template_id", + string="Variables", + store=True, + compute="_compute_variable_ids", + precompute=True, + readonly=False, + ) + button_ids = fields.One2many( + "mail.whatsapp.template.button", "template_id", string="Buttons" + ) _sql_constraints = [ ( @@ -70,6 +93,67 @@ class MailWhatsAppTemplate(models.Model): ) ] + @api.constrains("button_ids") + def _check_buttons(self): + for template in self: + if len(template.button_ids) > 10: + raise ValidationError(_("A maximum of 10 buttons is allowed.")) + url_buttons = template.button_ids.filtered( + lambda button: button.button_type == "url" + ) + phone_number_buttons = template.button_ids.filtered( + lambda button: button.button_type == "phone_number" + ) + if len(url_buttons) > 2: + raise ValidationError(_("A maximum of 2 URL buttons is allowed.")) + if len(phone_number_buttons) > 1: + raise ValidationError( + _("A maximum of 1 Call Number button is allowed.") + ) + + @api.constrains("variable_ids") + def _check_variables(self): + for template in self: + body_variables = template.variable_ids.filtered( + lambda var: var.line_type == "body" + ) + header_variables = template.variable_ids.filtered( + lambda var: var.line_type == "header" + ) + if len(header_variables) > 1: + raise ValidationError( + _("There should be exactly 1 variable in the header.") + ) + if header_variables and header_variables._extract_variable_index() != 1: + raise ValidationError( + _("Variable in the header should be used as {{1}}") + ) + variable_indices = sorted( + var._extract_variable_index() for var in body_variables + ) + if len(variable_indices) > 0 and ( + variable_indices[0] != 1 or variable_indices[-1] != len(body_variables) + ): + missing = ( + next( + ( + index + for index in range(1, len(body_variables)) + if variable_indices[index - 1] + 1 + != variable_indices[index] + ), + 0, + ) + + 1 + ) + raise ValidationError( + _( + "Body variables should start at 1 and not skip any number, " + "missing %d", + missing, + ) + ) + @api.depends("name", "state", "template_uid") def _compute_template_name(self): for template in self: @@ -80,6 +164,53 @@ def _compute_template_name(self): r"\W+", "_", self.env["ir.http"]._slugify(template.name or "") ) + @api.depends("header", "body") + def _compute_variable_ids(self): + for template in self: + to_remove = self.env["mail.whatsapp.template.variable"] + to_keep = self.env["mail.whatsapp.template.variable"] + new_values = [] + header_variables = list(re.findall(REG_VARIABLE, template.header or "")) + body_variables = set(re.findall(REG_VARIABLE, template.body or "")) + # header + current_header_variable = template.variable_ids.filtered( + lambda line: line.line_type == "header" + ) + if header_variables and not current_header_variable: + new_values.append( + { + "name": header_variables[0], + "line_type": "header", + "template_id": template.id, + } + ) + elif not header_variables and current_header_variable: + to_remove += current_header_variable + elif current_header_variable: + to_keep += current_header_variable + # body + current_body_variables = template.variable_ids.filtered( + lambda line: line.line_type == "body" + ) + new_body_variable_names = [ + var_name + for var_name in body_variables + if var_name not in current_body_variables.mapped("name") + ] + deleted_variables = current_body_variables.filtered( + lambda var, body_variables=body_variables: var.name + not in body_variables + ) + new_values += [ + {"name": var_name, "line_type": "body", "template_id": template.id} + for var_name in set(new_body_variable_names) + ] + to_remove += deleted_variables + to_keep += current_body_variables - deleted_variables + template.variable_ids = [(3, to_remove.id) for to_remove in to_remove] + [ + Command.create(vals) for vals in new_values + ] + def button_back2draft(self): self.write({"state": "draft"}) @@ -123,23 +254,39 @@ def _prepare_values_to_export(self): } def _prepare_components_to_export(self): - components = [{"type": "BODY", "text": self.body}] + body_component = {"type": "BODY", "text": self.body} + body_params = self.variable_ids.filtered(lambda line: line.line_type == "body") + if body_params: + body_component["example"] = { + "body_text": [body_params.mapped("sample_value")] + } + components = [body_component] if self.header: - components.append( - { - "type": "HEADER", - "format": "text", - "text": self.header, - } + header_component = {"type": "HEADER", "format": "TEXT", "text": self.header} + header_params = self.variable_ids.filtered( + lambda line: line.line_type == "header" ) - if self.footer: - components.append( - { - "type": "FOOTER", - "text": self.footer, + if header_params: + header_component["example"] = { + "header_text": header_params.mapped("sample_value") } - ) - # TODO: add more components(buttons, location, etc) + components.append(header_component) + if self.footer: + components.append({"type": "FOOTER", "text": self.footer}) + buttons = [] + for button in self.button_ids: + button_data = {"type": button.button_type.upper(), "text": button.name} + if button.button_type == "url": + button_data["url"] = button.website_url + if button.url_type == "dynamic": + button_data["url"] += "{{1}}" + button_data["example"] = button.variable_ids[0].sample_value + elif button.button_type == "phone_number": + button_data["phone_number"] = button.call_number + buttons.append(button_data) + if buttons: + components.append({"type": "BUTTONS", "buttons": buttons}) + # TODO: add more components(location, etc) return components def button_sync_template(self): @@ -185,7 +332,382 @@ def _prepare_values_to_import(self, gateway, json_data): vals["body"] = component["text"] elif component["type"] == "FOOTER": vals["footer"] = component["text"] + elif component["type"] == "BUTTONS": + for index, button in enumerate(component["buttons"]): + if button["type"] in ("URL", "PHONE_NUMBER", "QUICK_REPLY"): + button_vals = { + "sequence": index, + "name": button["text"], + "button_type": button["type"].lower(), + "call_number": button.get("phone_number"), + "website_url": button.get("url"), + } + vals.setdefault("button_ids", []) + button = self.button_ids.filtered( + lambda btn, button=button: btn.name == button["text"] + ) + if button: + vals["button_ids"].append( + Command.update(button.id, button_vals) + ) + else: + vals["button_ids"].append(Command.create(button_vals)) else: is_supported = False vals["is_supported"] = is_supported return vals + + def _prepare_header_component(self, variable_ids_value): + header = [] + if self.header and variable_ids_value.get("header-{{1}}"): + value = variable_ids_value.get("header-{{1}}") or (self.header or {}) or "" + header = {"type": "header", "parameters": [{"type": "text", "text": value}]} + return header + + def _prepare_body_components(self, variable_ids_value): + if not self.variable_ids: + return None + parameters = [] + for body_val in self.variable_ids.filtered( + lambda line: line.line_type == "body" + ): + parameters.append( + { + "type": "text", + "text": variable_ids_value.get( + f"{body_val.line_type}-{body_val.name}" + ) + or " ", + } + ) + return {"type": "body", "parameters": parameters} + + def _prepare_button_components(self, variable_ids_value): + components = [] + dynamic_buttons = self.button_ids.filtered( + lambda line: line.url_type == "dynamic" + ) + index = {button: i for i, button in enumerate(self.button_ids)} + for button in dynamic_buttons: + dynamic_url = button.website_url + value = variable_ids_value.get(f"button-{button.name}") or " " + value = value.replace(dynamic_url, "").lstrip("/") + components.append( + { + "type": "button", + "sub_type": "url", + "index": index.get(button), + "parameters": [{"type": "text", "text": value}], + } + ) + return components + + def prepare_value_to_send(self): + self.ensure_one() + model_name = self.model_id.model + rec_id = self.env.context.get("default_res_id") + if rec_id is None: + rec_ids = self.env.context.get("res_id") + if rec_ids: + rec_id = rec_ids + else: + rec_id = None + if model_name and rec_id: + record = self.env[model_name].browse(int(rec_id)) + components = [] + variable_ids_value = self.variable_ids._get_variables_value(record) + # generate components + header = self._prepare_header_component(variable_ids_value=variable_ids_value) + body = self._prepare_body_components(variable_ids_value=variable_ids_value) + buttons = self._prepare_button_components(variable_ids_value=variable_ids_value) + if header: + components.append(header) + if body: + components.append(body) + components.extend(buttons) + return components + + def render_body_message(self): + self.ensure_one() + model_name = self.model_id.model + rec_id = self.env.context.get("default_res_id") + if rec_id is None: + rec_ids = self.env.context.get("default_res_ids") + if isinstance(rec_ids, list) and rec_ids: + rec_id = rec_ids[0] + else: + rec_id = None + if model_name and rec_id: + record = self.env[model_name].browse(int(rec_id)) + header = "" + if self.header: + header = self.header + header_vars = self.variable_ids.filtered(lambda v: v.line_type == "header") + for i, var in enumerate(header_vars, start=1): + placeholder = f"{{{{{i}}}}}" + value = var._get_variables_value(record).get( + f"header-{placeholder}", "" + ) + header = header.replace(placeholder, str(value)) + body = self.body or "" + body_vars = self.variable_ids.filtered(lambda v: v.line_type == "body") + for i, var in enumerate(body_vars, start=1): + placeholder = f"{{{{{i}}}}}" + value = var._get_variables_value(record).get(f"body-{placeholder}", "") + body = body.replace(placeholder, str(value)) + message = f"*{header}*\n\n{body}" if header else body + return message + + +class MailWhatsAppTemplateVariable(models.Model): + _name = "mail.whatsapp.template.variable" + _description = "WhatsApp Template Variable" + _order = "line_type desc, name, id" + + name = fields.Char(string="Placeholder", required=True) + template_id = fields.Many2one( + comodel_name="mail.whatsapp.template", required=True, ondelete="cascade" + ) + model = fields.Char(string="Model Name", related="template_id.model") + line_type = fields.Selection( + [("header", "Header"), ("body", "Body"), ("button", "Button")], required=True + ) + field_name = fields.Char(string="Field") + sample_value = fields.Char(default="Sample Value", required=True) + button_id = fields.Many2one("mail.whatsapp.template.button", ondelete="cascade") + + _sql_constraints = [ + ( + "name_type_template_unique", + "UNIQUE(name, line_type, template_id,button_id)", + "Variable names must be unique by template", + ), + ] + + @api.constrains("field_name") + def _check_field_name(self): + failing = self.browse() + missing = self.filtered(lambda variable: not variable.field_name) + if missing: + raise ValidationError( + _( + "Field template variables %(variables)s " + "must be associated with a field.", + variables=", ".join(missing.mapped("name")), + ) + ) + for variable in self: + model = self.env[variable.model] + if not model.has_access("read"): + model_description = ( + self.env["ir.model"]._get(variable.model).display_name + ) + raise ValidationError( + _("You can not select field of %(model)s.", model=model_description) + ) + try: + variable._extract_value_from_field_path(model) + except UserError: + failing += variable + if failing: + model_description = ( + self.env["ir.model"]._get(failing.mapped("model")[0]).display_name + ) + raise ValidationError( + _( + "Variables %(field_names)s do not seem to be valid field path " + "for model %(model_name)s.", + field_names=", ".join(failing.mapped("field_name")), + model_name=model_description, + ) + ) + + @api.constrains("name") + def _check_name(self): + for variable in self: + if not variable._extract_variable_index(): + raise ValidationError( + _( + "Template variable should be in format {{number}}. " + "Cannot parse '%(placeholder)s'", + placeholder=variable.name, + ) + ) + + @api.depends("line_type", "name") + def _compute_display_name(self): + type_names = dict(self._fields["line_type"]._description_selection(self.env)) + for variable in self: + type_name = type_names[variable.line_type or "body"] + variable.display_name = ( + type_name + if variable.line_type == "header" + else f"{type_name} - {variable.name}" + ) + + @api.onchange("model") + def _onchange_model_id(self): + self.field_name = False + + def _get_variables_value(self, record): + value_by_name = {} + for variable in self: + value = variable._extract_value_from_field_path(record) + value_str = value and str(value) or "" + value_key = f"{variable.line_type}-{variable.name}" + value_by_name[value_key] = value_str + return value_by_name + + def _extract_variable_index(self): + """Extract variable index, located between '{{}}' markers.""" + self.ensure_one() + try: + return int(self.name.replace("{{", "").replace("}}", "")) + except ValueError: + return None + + def _extract_value_from_field_path(self, record): + field_path = self.field_name + if not field_path: + return "" + try: + field_value = record.mapped(field_path) + except Exception as err: + raise UserError( + _( + "We were not able to fetch value of field: %(field)s", + field=field_path, + ) + ) from err + if isinstance(field_value, models.Model): + return " ".join((value.display_name or "") for value in field_value) + # find last field / last model when having chained fields + # e.g. 'partner_id.country_id.state' -> ['partner_id.country_id', 'state'] + field_path_models = field_path.rsplit(".", 1) + if len(field_path_models) > 1: + last_model_path, last_fname = field_path_models + last_model = record.mapped(last_model_path) + else: + last_model, last_fname = record, field_path + last_field = last_model._fields[last_fname] + # return value instead of the key + if last_field.type == "selection": + return " ".join( + last_field.convert_to_export(value, last_model) for value in field_value + ) + return " ".join( + str(value if value is not False and value is not None else "") + for value in field_value + ) + + +class MailWhatsAppTemplateButton(models.Model): + _name = "mail.whatsapp.template.button" + _description = "WhatsApp Template Button" + _order = "sequence,id" + + sequence = fields.Integer() + name = fields.Char(string="Button Text", size=25) + template_id = fields.Many2one( + comodel_name="mail.whatsapp.template", required=True, ondelete="cascade" + ) + button_type = fields.Selection( + [ + ("quick_reply", "Quick Reply"), + ("url", "Visit Website"), + ("phone_number", "Call Number"), + ], + string="Type", + required=True, + default="quick_reply", + ) + url_type = fields.Selection( + [("static", "Static"), ("dynamic", "Dynamic")], + default="static", + ) + website_url = fields.Char() + call_number = fields.Char() + variable_ids = fields.One2many( + "mail.whatsapp.template.variable", + "button_id", + compute="_compute_variable_ids", + precompute=True, + store=True, + copy=True, + ) + + _sql_constraints = [ + ( + "unique_name_per_template", + "UNIQUE(name, template_id)", + "Button name must be unique by template", + ) + ] + + @api.constrains("button_type", "url_type", "website_url") + def _validate_website_url(self): + for button in self.filtered(lambda button: button.button_type == "url"): + parsed_url = urlparse(button.website_url) + if not (parsed_url.scheme in {"http", "https"} and parsed_url.netloc): + raise ValidationError( + _( + "Please enter a valid URL in the format 'https://www.example.com'." + ) + ) + + @api.constrains("call_number") + def _validate_call_number(self): + for button in self: + if button.button_type == "phone_number": + phone_validation.phone_format(button.call_number, False, False) + + @api.depends("button_type", "url_type", "website_url", "name") + def _compute_variable_ids(self): + dynamic_urls = self.filtered( + lambda button: button.button_type == "url" and button.url_type == "dynamic" + ) + to_clear = self - dynamic_urls + for button in dynamic_urls: + if button.variable_ids: + button.variable_ids = [ + ( + 1, + button.variable_ids[0].id, + { + "sample_value": button.website_url, + "line_type": "button", + "name": button.name, + "button_id": button.id, + "template_id": button.template_id.id, + }, + ), + ] + else: + button.variable_ids = [ + ( + 0, + 0, + { + "sample_value": button.website_url, + "line_type": "button", + "name": button.name, + "button_id": button.id, + "template_id": button.template_id.id, + }, + ), + ] + if to_clear: + to_clear.variable_ids = [(5, 0)] + + def check_variable_ids(self): + for button in self: + if len(button.variable_ids) > 1: + raise ValidationError(_("Buttons may only contain one placeholder.")) + if button.variable_ids and button.url_type != "dynamic": + raise ValidationError(_("Only dynamic urls may have a placeholder.")) + elif button.url_type == "dynamic" and not button.variable_ids: + raise ValidationError(_("All dynamic urls must have a placeholder.")) + if button.variable_ids.name != "{{1}}": + raise ValidationError( + _("The placeholder for a button can only be {{1}}.") + ) diff --git a/mail_gateway_whatsapp/security/ir.model.access.csv b/mail_gateway_whatsapp/security/ir.model.access.csv index 347b773514..41ae7f726c 100644 --- a/mail_gateway_whatsapp/security/ir.model.access.csv +++ b/mail_gateway_whatsapp/security/ir.model.access.csv @@ -2,3 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_whatsapp_composer,access.whatsapp.composer,model_whatsapp_composer,base.group_user,1,1,1,0 access_mail_whatsapp_template_group_system,mail_whatsapp_template_group_system,model_mail_whatsapp_template,base.group_system,1,1,1,1 access_mail_whatsapp_template_group_user,mail_whatsapp_template_group_user,model_mail_whatsapp_template,base.group_user,1,0,0,0 +access_mail_whatsapp_template_button_group_system,access_mail_whatsapp_template_button_group_system,model_mail_whatsapp_template_button,base.group_system,1,1,1,1 +access_mail_whatsapp_template_button_group_user,access_mail_whatsapp_template_button_group_user,model_mail_whatsapp_template_button,base.group_user,1,0,0,0 +access_mail_whatsapp_template_variable_group_system,access_mail_whatsapp_template_variable_group_system,model_mail_whatsapp_template_variable,base.group_system,1,1,1,1 +access_mail_whatsapp_template_variable_group_user,access_mail_whatsapp_template_variable_group_user,model_mail_whatsapp_template_variable,base.group_user,1,0,0,0 diff --git a/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py b/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py index d1e8059fd1..06cb4d620a 100644 --- a/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py +++ b/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py @@ -8,6 +8,7 @@ from markupsafe import Markup +from odoo import Command from odoo.exceptions import UserError from odoo.tests import Form, RecordCapturer from odoo.tests.common import tagged @@ -382,3 +383,164 @@ def test_compose(self): post_mock.assert_called() channel.invalidate_recordset() self.assertTrue(channel.message_ids) + + def test_send_message_with_variable(self): + ctx = { + "default_res_model": self.partner._name, + "default_res_id": self.partner.id, + "default_number_field_name": "mobile", + "default_composition_mode": "comment", + "default_gateway_id": self.gateway.id, + } + tmpl_with_vars = self.env["mail.whatsapp.template"].create( + { + "name": "Partner Vars", + "category": "utility", + "language": "es", + "header": "Hi {{1}}", + "body": "Name: {{1}} · Tel: {{2}}", + "gateway_id": self.gateway.id, + "variable_ids": [Command.clear()], + "state": "approved", + "is_supported": True, + "model_id": self.env["ir.model"]._get("res.partner").id, + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{1}}", + "line_type": "header", + "template_id": tmpl_with_vars.id, + "field_name": "name", + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{1}}", + "line_type": "body", + "template_id": tmpl_with_vars.id, + "field_name": "name", + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{2}}", + "line_type": "body", + "template_id": tmpl_with_vars.id, + "field_name": "mobile", + } + ) + self.env["mail.whatsapp.template.button"].create( + { + "name": "mobile", + "button_type": "phone_number", + "template_id": tmpl_with_vars.id, + "call_number": "+34666555444", + } + ) + self.assertEqual(len(tmpl_with_vars.variable_ids), 3) + self.assertEqual(len(tmpl_with_vars.button_ids), 1) + self.gateway.whatsapp_account_id = "123456" + form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx)) + form_composer.template_id = tmpl_with_vars + self.assertTrue(form_composer.is_required_template) + self.assertTrue(form_composer._get_modifier("template_id", "required")) + composer = form_composer.save() + channel = self.partner._whatsapp_get_channel( + composer.number_field_name, composer.gateway_id + ) + message_domain = [ + ("gateway_type", "=", "whatsapp"), + ("model", "=", channel._name), + ("res_id", "=", channel.id), + ] + with ( + RecordCapturer(self.env["mail.message"], message_domain) as capture, + patch("requests.post") as post_mock, + ): + post_mock.return_value = MagicMock() + composer.action_send_whatsapp() + # Assertions + self.assertEqual(len(capture.records), 1) + channel.invalidate_recordset() + self.assertTrue(channel.message_ids) + + def test_send_message_with_dynamic_button(self): + ctx = { + "default_res_model": self.partner._name, + "default_res_id": self.partner.id, + "default_number_field_name": "mobile", + "default_composition_mode": "comment", + "default_gateway_id": self.gateway.id, + } + tmpl_with_vars_dynamic = self.env["mail.whatsapp.template"].create( + { + "name": "Dynamic Button", + "category": "utility", + "language": "es", + "body": "Name: {{1}} · Tel: {{2}}", + "gateway_id": self.gateway.id, + "variable_ids": [Command.clear()], + "state": "approved", + "is_supported": True, + "model_id": self.env["ir.model"]._get("res.partner").id, + } + ) + self.assertEqual(len(tmpl_with_vars_dynamic.variable_ids), 0) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{1}}", + "line_type": "body", + "template_id": tmpl_with_vars_dynamic.id, + "field_name": "name", + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{2}}", + "line_type": "body", + "template_id": tmpl_with_vars_dynamic.id, + "field_name": "mobile", + } + ) + self.env["mail.whatsapp.template.button"].create( + { + "name": "1", + "button_type": "url", + "url_type": "dynamic", + "template_id": tmpl_with_vars_dynamic.id, + "website_url": "https://www.odoo.com", + } + ) + self.assertEqual(len(tmpl_with_vars_dynamic.variable_ids), 3) + button = tmpl_with_vars_dynamic.variable_ids.search( + [ + ("line_type", "=", "button"), + ("template_id", "=", tmpl_with_vars_dynamic.id), + ] + ) + button.field_name = "name" + self.gateway.whatsapp_account_id = "123456" + form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx)) + form_composer.template_id = tmpl_with_vars_dynamic + self.assertTrue(form_composer.is_required_template) + self.assertTrue(form_composer._get_modifier("template_id", "required")) + composer = form_composer.save() + channel = self.partner._whatsapp_get_channel( + composer.number_field_name, composer.gateway_id + ) + message_domain = [ + ("gateway_type", "=", "whatsapp"), + ("model", "=", channel._name), + ("res_id", "=", channel.id), + ] + with ( + RecordCapturer(self.env["mail.message"], message_domain) as capture, + patch("requests.post") as post_mock, + ): + post_mock.return_value = MagicMock() + composer.action_send_whatsapp() + # Assertions + self.assertEqual(len(capture.records), 1) + channel.invalidate_recordset() + self.assertTrue(channel.message_ids) diff --git a/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py b/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py index b7d30045d7..e459887c53 100644 --- a/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py +++ b/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py @@ -6,6 +6,7 @@ import requests +from odoo import Command from odoo.exceptions import UserError from odoo.tests.common import tagged @@ -121,7 +122,7 @@ def _patch_request_post(url, *args, **kwargs): template_2 = self.gateway.whatsapp_template_ids.filtered( lambda t: t.template_uid == "0987654321" ) - self.assertFalse(template_2.is_supported) + self.assertTrue(template_2.is_supported) self.assertEqual(template_2.template_name, "test_with_buttons") self.assertEqual(template_2.category, "marketing") self.assertEqual(template_2.language, "es") @@ -129,7 +130,6 @@ def _patch_request_post(url, *args, **kwargs): self.assertEqual(template_2.header, "Header 2") self.assertEqual(template_2.body, "Body 2") self.assertFalse(template_2.footer) - self.assertFalse(template_2.is_supported) def test_export_template(self): def _patch_request_post(url, *args, **kwargs): @@ -168,3 +168,73 @@ def _patch_request_get(url, *args, **kwargs): with patch.object(requests, "get", _patch_request_get): new_template.button_sync_template() self.assertEqual(new_template.footer, "Footer changed") + + def test_prepare_values_template_send(self): + partner = self.env["res.partner"].create( + { + "name": "Ada Lovelace", + "mobile": "+34900111222", + } + ) + ctx = { + "default_res_model": partner._name, + "default_res_id": partner.id, + } + tmpl = self.env["mail.whatsapp.template"].create( + { + "name": "Test Render", + "category": "utility", + "language": "es", + "header": "Hi {{1}}", + "body": "Name: {{1}} · Tel: {{2}}", + "gateway_id": self.gateway.id, + "variable_ids": [Command.clear()], + "state": "approved", + "is_supported": True, + "model_id": self.env["ir.model"]._get("res.partner").id, + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{1}}", + "line_type": "header", + "template_id": tmpl.id, + "field_name": "name", + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{1}}", + "line_type": "body", + "template_id": tmpl.id, + "field_name": "name", + } + ) + self.env["mail.whatsapp.template.variable"].create( + { + "name": "{{2}}", + "line_type": "body", + "template_id": tmpl.id, + "field_name": "mobile", + } + ) + self.env["mail.whatsapp.template.button"].create( + { + "name": "mobile", + "button_type": "phone_number", + "template_id": tmpl.id, + "call_number": "+34666555444", + } + ) + components = tmpl.with_context(**ctx).prepare_value_to_send() + + header = next(c for c in components if c["type"].upper() == "HEADER") + self.assertEqual([p["type"] for p in header.get("parameters", [])], ["text"]) + self.assertEqual(header["parameters"][0]["text"], partner.name) + + body = next(c for c in components if c["type"].upper() == "BODY") + self.assertEqual( + [p["type"] for p in body.get("parameters", [])], ["text", "text"] + ) + self.assertEqual(body["parameters"][0]["text"], partner.name) + self.assertEqual(body["parameters"][1]["text"], partner.mobile or "") diff --git a/mail_gateway_whatsapp/tools/const.py b/mail_gateway_whatsapp/tools/const.py index fb95ee302d..b02ee7202a 100644 --- a/mail_gateway_whatsapp/tools/const.py +++ b/mail_gateway_whatsapp/tools/const.py @@ -73,3 +73,5 @@ ("vi", "Vietnamese"), ("zu", "Zulu"), ] + +REG_VARIABLE = r"{{[1-9][0-9]*}}" diff --git a/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml b/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml index d495f32273..a3e88b0742 100644 --- a/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml +++ b/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml @@ -84,6 +84,7 @@ readonly="state != 'draft'" options="{'no_create': True}" /> + @@ -104,6 +105,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py b/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py index 143a35efbd..cd24a1b094 100644 --- a/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py +++ b/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py @@ -13,16 +13,22 @@ class MailComposeGatewayMessage(models.TransientModel): "mail.whatsapp.template", domain="""[ ('state', '=', 'approved'), - ('is_supported', '=', True) + ('is_supported', '=', True), + ('model', '=', model) ]""", ) @api.onchange("whatsapp_template_id") def onchange_whatsapp_template_id(self): if self.whatsapp_template_id: - self.body = markupsafe.Markup(self.whatsapp_template_id.body) + self.body = markupsafe.Markup( + self.whatsapp_template_id.render_body_message() + ) def _action_send_mail(self, auto_commit=False): if self.whatsapp_template_id: - self = self.with_context(whatsapp_template_id=self.whatsapp_template_id.id) + self = self.with_context( + whatsapp_template_id=self.whatsapp_template_id.id, + res_id=int(self.res_ids.strip("[]")), + ) return super()._action_send_mail(auto_commit=auto_commit) diff --git a/mail_gateway_whatsapp/wizards/whatsapp_composer.py b/mail_gateway_whatsapp/wizards/whatsapp_composer.py index 3fb6fbc950..e5310229cf 100644 --- a/mail_gateway_whatsapp/wizards/whatsapp_composer.py +++ b/mail_gateway_whatsapp/wizards/whatsapp_composer.py @@ -23,7 +23,8 @@ class WhatsappComposer(models.TransientModel): domain="""[ ('gateway_id', '=', gateway_id), ('state', '=', 'approved'), - ('is_supported', '=', True) + ('is_supported', '=', True), + ('model', '=', res_model) ]""", ) body = fields.Text("Message") @@ -68,7 +69,7 @@ def onchange_gateway_id(self): @api.onchange("template_id") def onchange_template_id(self): if self.template_id: - self.body = self.template_id.body + self.body = self.template_id.render_body_message() @api.model def default_get(self, fields):