odoo-addon-mail-gateway-whatsapp 17.0.1.0.1__py3-none-any.whl → 17.0.1.1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/mail_gateway_whatsapp/README.rst +1 -1
- odoo/addons/mail_gateway_whatsapp/__manifest__.py +1 -1
- odoo/addons/mail_gateway_whatsapp/i18n/es.po +273 -12
- odoo/addons/mail_gateway_whatsapp/i18n/it.po +273 -12
- odoo/addons/mail_gateway_whatsapp/i18n/mail_gateway_whatsapp.pot +267 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py +1 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_whatsapp_template.py +539 -17
- odoo/addons/mail_gateway_whatsapp/security/ir.model.access.csv +4 -0
- odoo/addons/mail_gateway_whatsapp/static/description/index.html +1 -1
- odoo/addons/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py +160 -0
- odoo/addons/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py +72 -2
- odoo/addons/mail_gateway_whatsapp/tools/const.py +2 -0
- odoo/addons/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml +54 -0
- odoo/addons/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py +9 -3
- odoo/addons/mail_gateway_whatsapp/wizards/whatsapp_composer.py +3 -2
- {odoo_addon_mail_gateway_whatsapp-17.0.1.0.1.dist-info → odoo_addon_mail_gateway_whatsapp-17.0.1.1.0.1.dist-info}/METADATA +2 -2
- {odoo_addon_mail_gateway_whatsapp-17.0.1.0.1.dist-info → odoo_addon_mail_gateway_whatsapp-17.0.1.1.0.1.dist-info}/RECORD +19 -19
- {odoo_addon_mail_gateway_whatsapp-17.0.1.0.1.dist-info → odoo_addon_mail_gateway_whatsapp-17.0.1.1.0.1.dist-info}/WHEEL +0 -0
- {odoo_addon_mail_gateway_whatsapp-17.0.1.0.1.dist-info → odoo_addon_mail_gateway_whatsapp-17.0.1.1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# Copyright 2024 Tecnativa - Carlos López
|
|
2
2
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
3
|
import re
|
|
4
|
+
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
6
|
import requests
|
|
6
7
|
from werkzeug.urls import url_join
|
|
7
8
|
|
|
8
|
-
from odoo import api, fields, models
|
|
9
|
-
from odoo.exceptions import UserError
|
|
9
|
+
from odoo import Command, _, api, fields, models
|
|
10
|
+
from odoo.exceptions import UserError, ValidationError
|
|
10
11
|
from odoo.tools import ustr
|
|
11
12
|
|
|
12
13
|
from odoo.addons.http_routing.models.ir_http import slugify
|
|
14
|
+
from odoo.addons.phone_validation.tools import phone_validation
|
|
13
15
|
|
|
14
|
-
from ..tools.const import supported_languages
|
|
16
|
+
from ..tools.const import REG_VARIABLE, supported_languages
|
|
15
17
|
from .mail_gateway import BASE_URL
|
|
16
18
|
|
|
17
19
|
|
|
@@ -63,6 +65,26 @@ class MailWhatsAppTemplate(models.Model):
|
|
|
63
65
|
company_id = fields.Many2one(
|
|
64
66
|
"res.company", related="gateway_id.company_id", store=True
|
|
65
67
|
)
|
|
68
|
+
model_id = fields.Many2one(
|
|
69
|
+
string="Applies to",
|
|
70
|
+
comodel_name="ir.model",
|
|
71
|
+
default=lambda self: self.env["ir.model"]._get_id("res.partner"),
|
|
72
|
+
required=True,
|
|
73
|
+
ondelete="cascade",
|
|
74
|
+
)
|
|
75
|
+
model = fields.Char(string="Related model", related="model_id.model", store=True)
|
|
76
|
+
variable_ids = fields.One2many(
|
|
77
|
+
"mail.whatsapp.template.variable",
|
|
78
|
+
"template_id",
|
|
79
|
+
string="Variables",
|
|
80
|
+
store=True,
|
|
81
|
+
compute="_compute_variable_ids",
|
|
82
|
+
precompute=True,
|
|
83
|
+
readonly=False,
|
|
84
|
+
)
|
|
85
|
+
button_ids = fields.One2many(
|
|
86
|
+
"mail.whatsapp.template.button", "template_id", string="Buttons"
|
|
87
|
+
)
|
|
66
88
|
|
|
67
89
|
_sql_constraints = [
|
|
68
90
|
(
|
|
@@ -72,6 +94,67 @@ class MailWhatsAppTemplate(models.Model):
|
|
|
72
94
|
)
|
|
73
95
|
]
|
|
74
96
|
|
|
97
|
+
@api.constrains("button_ids")
|
|
98
|
+
def _check_buttons(self):
|
|
99
|
+
for template in self:
|
|
100
|
+
if len(template.button_ids) > 10:
|
|
101
|
+
raise ValidationError(_("A maximum of 10 buttons is allowed."))
|
|
102
|
+
url_buttons = template.button_ids.filtered(
|
|
103
|
+
lambda button: button.button_type == "url"
|
|
104
|
+
)
|
|
105
|
+
phone_number_buttons = template.button_ids.filtered(
|
|
106
|
+
lambda button: button.button_type == "phone_number"
|
|
107
|
+
)
|
|
108
|
+
if len(url_buttons) > 2:
|
|
109
|
+
raise ValidationError(_("A maximum of 2 URL buttons is allowed."))
|
|
110
|
+
if len(phone_number_buttons) > 1:
|
|
111
|
+
raise ValidationError(
|
|
112
|
+
_("A maximum of 1 Call Number button is allowed.")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@api.constrains("variable_ids")
|
|
116
|
+
def _check_variables(self):
|
|
117
|
+
for template in self:
|
|
118
|
+
body_variables = template.variable_ids.filtered(
|
|
119
|
+
lambda var: var.line_type == "body"
|
|
120
|
+
)
|
|
121
|
+
header_variables = template.variable_ids.filtered(
|
|
122
|
+
lambda var: var.line_type == "header"
|
|
123
|
+
)
|
|
124
|
+
if len(header_variables) > 1:
|
|
125
|
+
raise ValidationError(
|
|
126
|
+
_("There should be exactly 1 variable in the header.")
|
|
127
|
+
)
|
|
128
|
+
if header_variables and header_variables._extract_variable_index() != 1:
|
|
129
|
+
raise ValidationError(
|
|
130
|
+
_("Variable in the header should be used as {{1}}")
|
|
131
|
+
)
|
|
132
|
+
variable_indices = sorted(
|
|
133
|
+
var._extract_variable_index() for var in body_variables
|
|
134
|
+
)
|
|
135
|
+
if len(variable_indices) > 0 and (
|
|
136
|
+
variable_indices[0] != 1 or variable_indices[-1] != len(body_variables)
|
|
137
|
+
):
|
|
138
|
+
missing = (
|
|
139
|
+
next(
|
|
140
|
+
(
|
|
141
|
+
index
|
|
142
|
+
for index in range(1, len(body_variables))
|
|
143
|
+
if variable_indices[index - 1] + 1
|
|
144
|
+
!= variable_indices[index]
|
|
145
|
+
),
|
|
146
|
+
0,
|
|
147
|
+
)
|
|
148
|
+
+ 1
|
|
149
|
+
)
|
|
150
|
+
raise ValidationError(
|
|
151
|
+
_(
|
|
152
|
+
"Body variables should start at 1 and not skip any number, "
|
|
153
|
+
"missing %d",
|
|
154
|
+
missing,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
75
158
|
@api.depends("name", "state", "template_uid")
|
|
76
159
|
def _compute_template_name(self):
|
|
77
160
|
for template in self:
|
|
@@ -82,6 +165,54 @@ class MailWhatsAppTemplate(models.Model):
|
|
|
82
165
|
r"\W+", "_", slugify(template.name or "")
|
|
83
166
|
)
|
|
84
167
|
|
|
168
|
+
@api.depends("header", "body")
|
|
169
|
+
def _compute_variable_ids(self):
|
|
170
|
+
for template in self:
|
|
171
|
+
to_remove = self.env["mail.whatsapp.template.variable"]
|
|
172
|
+
to_keep = self.env["mail.whatsapp.template.variable"]
|
|
173
|
+
new_values = []
|
|
174
|
+
header_variables = list(re.findall(REG_VARIABLE, template.header or ""))
|
|
175
|
+
body_variables = set(re.findall(REG_VARIABLE, template.body or ""))
|
|
176
|
+
# header
|
|
177
|
+
current_header_variable = template.variable_ids.filtered(
|
|
178
|
+
lambda line: line.line_type == "header"
|
|
179
|
+
)
|
|
180
|
+
if header_variables and not current_header_variable:
|
|
181
|
+
new_values.append(
|
|
182
|
+
{
|
|
183
|
+
"name": header_variables[0],
|
|
184
|
+
"line_type": "header",
|
|
185
|
+
"template_id": template.id,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
elif not header_variables and current_header_variable:
|
|
189
|
+
to_remove += current_header_variable
|
|
190
|
+
elif current_header_variable:
|
|
191
|
+
to_keep += current_header_variable
|
|
192
|
+
# body
|
|
193
|
+
current_body_variables = template.variable_ids.filtered(
|
|
194
|
+
lambda line: line.line_type == "body"
|
|
195
|
+
)
|
|
196
|
+
new_body_variable_names = [
|
|
197
|
+
var_name
|
|
198
|
+
for var_name in body_variables
|
|
199
|
+
if var_name not in current_body_variables.mapped("name")
|
|
200
|
+
]
|
|
201
|
+
deleted_variables = current_body_variables.filtered(
|
|
202
|
+
lambda var, body_variables=body_variables: var.name
|
|
203
|
+
not in body_variables
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
new_values += [
|
|
207
|
+
{"name": var_name, "line_type": "body", "template_id": template.id}
|
|
208
|
+
for var_name in set(new_body_variable_names)
|
|
209
|
+
]
|
|
210
|
+
to_remove += deleted_variables
|
|
211
|
+
to_keep += current_body_variables - deleted_variables
|
|
212
|
+
template.variable_ids = [(3, to_remove.id) for to_remove in to_remove] + [
|
|
213
|
+
Command.create(vals) for vals in new_values
|
|
214
|
+
]
|
|
215
|
+
|
|
85
216
|
def button_back2draft(self):
|
|
86
217
|
self.write({"state": "draft"})
|
|
87
218
|
|
|
@@ -125,23 +256,39 @@ class MailWhatsAppTemplate(models.Model):
|
|
|
125
256
|
}
|
|
126
257
|
|
|
127
258
|
def _prepare_components_to_export(self):
|
|
128
|
-
|
|
259
|
+
body_component = {"type": "BODY", "text": self.body}
|
|
260
|
+
body_params = self.variable_ids.filtered(lambda line: line.line_type == "body")
|
|
261
|
+
if body_params:
|
|
262
|
+
body_component["example"] = {
|
|
263
|
+
"body_text": [body_params.mapped("sample_value")]
|
|
264
|
+
}
|
|
265
|
+
components = [body_component]
|
|
129
266
|
if self.header:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"format": "text",
|
|
134
|
-
"text": self.header,
|
|
135
|
-
}
|
|
267
|
+
header_component = {"type": "HEADER", "format": "TEXT", "text": self.header}
|
|
268
|
+
header_params = self.variable_ids.filtered(
|
|
269
|
+
lambda line: line.line_type == "header"
|
|
136
270
|
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"type": "FOOTER",
|
|
141
|
-
"text": self.footer,
|
|
271
|
+
if header_params:
|
|
272
|
+
header_component["example"] = {
|
|
273
|
+
"header_text": header_params.mapped("sample_value")
|
|
142
274
|
}
|
|
143
|
-
)
|
|
144
|
-
|
|
275
|
+
components.append(header_component)
|
|
276
|
+
if self.footer:
|
|
277
|
+
components.append({"type": "FOOTER", "text": self.footer})
|
|
278
|
+
buttons = []
|
|
279
|
+
for button in self.button_ids:
|
|
280
|
+
button_data = {"type": button.button_type.upper(), "text": button.name}
|
|
281
|
+
if button.button_type == "url":
|
|
282
|
+
button_data["url"] = button.website_url
|
|
283
|
+
if button.url_type == "dynamic":
|
|
284
|
+
button_data["url"] += "{{1}}"
|
|
285
|
+
button_data["example"] = button.variable_ids[0].sample_value
|
|
286
|
+
elif button.button_type == "phone_number":
|
|
287
|
+
button_data["phone_number"] = button.call_number
|
|
288
|
+
buttons.append(button_data)
|
|
289
|
+
if buttons:
|
|
290
|
+
components.append({"type": "BUTTONS", "buttons": buttons})
|
|
291
|
+
# TODO: add more components(location, etc)
|
|
145
292
|
return components
|
|
146
293
|
|
|
147
294
|
def button_sync_template(self):
|
|
@@ -187,7 +334,382 @@ class MailWhatsAppTemplate(models.Model):
|
|
|
187
334
|
vals["body"] = component["text"]
|
|
188
335
|
elif component["type"] == "FOOTER":
|
|
189
336
|
vals["footer"] = component["text"]
|
|
337
|
+
elif component["type"] == "BUTTONS":
|
|
338
|
+
for index, button in enumerate(component["buttons"]):
|
|
339
|
+
if button["type"] in ("URL", "PHONE_NUMBER", "QUICK_REPLY"):
|
|
340
|
+
button_vals = {
|
|
341
|
+
"sequence": index,
|
|
342
|
+
"name": button["text"],
|
|
343
|
+
"button_type": button["type"].lower(),
|
|
344
|
+
"call_number": button.get("phone_number"),
|
|
345
|
+
"website_url": button.get("url"),
|
|
346
|
+
}
|
|
347
|
+
vals.setdefault("button_ids", [])
|
|
348
|
+
button = self.button_ids.filtered(
|
|
349
|
+
lambda btn, button=button: btn.name == button["text"]
|
|
350
|
+
)
|
|
351
|
+
if button:
|
|
352
|
+
vals["button_ids"].append(
|
|
353
|
+
Command.update(button.id, button_vals)
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
vals["button_ids"].append(Command.create(button_vals))
|
|
190
357
|
else:
|
|
191
358
|
is_supported = False
|
|
192
359
|
vals["is_supported"] = is_supported
|
|
193
360
|
return vals
|
|
361
|
+
|
|
362
|
+
def _prepare_header_component(self, variable_ids_value):
|
|
363
|
+
header = []
|
|
364
|
+
if self.header and variable_ids_value.get("header-{{1}}"):
|
|
365
|
+
value = variable_ids_value.get("header-{{1}}") or (self.header or {}) or ""
|
|
366
|
+
header = {"type": "header", "parameters": [{"type": "text", "text": value}]}
|
|
367
|
+
return header
|
|
368
|
+
|
|
369
|
+
def _prepare_body_components(self, variable_ids_value):
|
|
370
|
+
if not self.variable_ids:
|
|
371
|
+
return None
|
|
372
|
+
parameters = []
|
|
373
|
+
for body_val in self.variable_ids.filtered(
|
|
374
|
+
lambda line: line.line_type == "body"
|
|
375
|
+
):
|
|
376
|
+
parameters.append(
|
|
377
|
+
{
|
|
378
|
+
"type": "text",
|
|
379
|
+
"text": variable_ids_value.get(
|
|
380
|
+
f"{body_val.line_type}-{body_val.name}"
|
|
381
|
+
)
|
|
382
|
+
or " ",
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
return {"type": "body", "parameters": parameters}
|
|
386
|
+
|
|
387
|
+
def _prepare_button_components(self, variable_ids_value):
|
|
388
|
+
components = []
|
|
389
|
+
dynamic_buttons = self.button_ids.filtered(
|
|
390
|
+
lambda line: line.url_type == "dynamic"
|
|
391
|
+
)
|
|
392
|
+
index = {button: i for i, button in enumerate(self.button_ids)}
|
|
393
|
+
for button in dynamic_buttons:
|
|
394
|
+
dynamic_url = button.website_url
|
|
395
|
+
value = variable_ids_value.get(f"button-{button.name}") or " "
|
|
396
|
+
value = value.replace(dynamic_url, "").lstrip("/")
|
|
397
|
+
components.append(
|
|
398
|
+
{
|
|
399
|
+
"type": "button",
|
|
400
|
+
"sub_type": "url",
|
|
401
|
+
"index": index.get(button),
|
|
402
|
+
"parameters": [{"type": "text", "text": value}],
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
return components
|
|
406
|
+
|
|
407
|
+
def prepare_value_to_send(self):
|
|
408
|
+
self.ensure_one()
|
|
409
|
+
model_name = self.model_id.model
|
|
410
|
+
rec_id = self.env.context.get("default_res_id")
|
|
411
|
+
if rec_id is None:
|
|
412
|
+
rec_ids = self.env.context.get("res_id")
|
|
413
|
+
if rec_ids:
|
|
414
|
+
rec_id = rec_ids
|
|
415
|
+
else:
|
|
416
|
+
rec_id = None
|
|
417
|
+
if model_name and rec_id:
|
|
418
|
+
record = self.env[model_name].browse(int(rec_id))
|
|
419
|
+
components = []
|
|
420
|
+
variable_ids_value = self.variable_ids._get_variables_value(record)
|
|
421
|
+
# generate components
|
|
422
|
+
header = self._prepare_header_component(variable_ids_value=variable_ids_value)
|
|
423
|
+
body = self._prepare_body_components(variable_ids_value=variable_ids_value)
|
|
424
|
+
buttons = self._prepare_button_components(variable_ids_value=variable_ids_value)
|
|
425
|
+
if header:
|
|
426
|
+
components.append(header)
|
|
427
|
+
if body:
|
|
428
|
+
components.append(body)
|
|
429
|
+
components.extend(buttons)
|
|
430
|
+
return components
|
|
431
|
+
|
|
432
|
+
def render_body_message(self):
|
|
433
|
+
self.ensure_one()
|
|
434
|
+
model_name = self.model_id.model
|
|
435
|
+
rec_id = self.env.context.get("default_res_id")
|
|
436
|
+
if rec_id is None:
|
|
437
|
+
rec_ids = self.env.context.get("default_res_ids")
|
|
438
|
+
if isinstance(rec_ids, list) and rec_ids:
|
|
439
|
+
rec_id = rec_ids[0]
|
|
440
|
+
else:
|
|
441
|
+
rec_id = None
|
|
442
|
+
if model_name and rec_id:
|
|
443
|
+
record = self.env[model_name].browse(int(rec_id))
|
|
444
|
+
header = ""
|
|
445
|
+
if self.header:
|
|
446
|
+
header = self.header
|
|
447
|
+
header_vars = self.variable_ids.filtered(lambda v: v.line_type == "header")
|
|
448
|
+
for i, var in enumerate(header_vars, start=1):
|
|
449
|
+
placeholder = f"{{{{{i}}}}}"
|
|
450
|
+
value = var._get_variables_value(record).get(
|
|
451
|
+
f"header-{placeholder}", ""
|
|
452
|
+
)
|
|
453
|
+
header = header.replace(placeholder, str(value))
|
|
454
|
+
body = self.body or ""
|
|
455
|
+
body_vars = self.variable_ids.filtered(lambda v: v.line_type == "body")
|
|
456
|
+
for i, var in enumerate(body_vars, start=1):
|
|
457
|
+
placeholder = f"{{{{{i}}}}}"
|
|
458
|
+
value = var._get_variables_value(record).get(f"body-{placeholder}", "")
|
|
459
|
+
body = body.replace(placeholder, str(value))
|
|
460
|
+
message = f"*{header}*\n\n{body}" if header else body
|
|
461
|
+
return message
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class MailWhatsAppTemplateVariable(models.Model):
|
|
465
|
+
_name = "mail.whatsapp.template.variable"
|
|
466
|
+
_description = "WhatsApp Template Variable"
|
|
467
|
+
_order = "line_type desc, name, id"
|
|
468
|
+
|
|
469
|
+
name = fields.Char(string="Placeholder", required=True)
|
|
470
|
+
template_id = fields.Many2one(
|
|
471
|
+
comodel_name="mail.whatsapp.template", required=True, ondelete="cascade"
|
|
472
|
+
)
|
|
473
|
+
model = fields.Char(string="Model Name", related="template_id.model")
|
|
474
|
+
line_type = fields.Selection(
|
|
475
|
+
[("header", "Header"), ("body", "Body"), ("button", "Button")], required=True
|
|
476
|
+
)
|
|
477
|
+
field_name = fields.Char(string="Field")
|
|
478
|
+
sample_value = fields.Char(default="Sample Value", required=True)
|
|
479
|
+
button_id = fields.Many2one("mail.whatsapp.template.button", ondelete="cascade")
|
|
480
|
+
|
|
481
|
+
_sql_constraints = [
|
|
482
|
+
(
|
|
483
|
+
"name_type_template_unique",
|
|
484
|
+
"UNIQUE(name, line_type, template_id,button_id)",
|
|
485
|
+
"Variable names must be unique by template",
|
|
486
|
+
),
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
@api.constrains("field_name")
|
|
490
|
+
def _check_field_name(self):
|
|
491
|
+
failing = self.browse()
|
|
492
|
+
missing = self.filtered(lambda variable: not variable.field_name)
|
|
493
|
+
if missing:
|
|
494
|
+
raise ValidationError(
|
|
495
|
+
_(
|
|
496
|
+
"Field template variables %(variables)s "
|
|
497
|
+
"must be associated with a field.",
|
|
498
|
+
variables=", ".join(missing.mapped("name")),
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
for variable in self:
|
|
502
|
+
model = self.env[variable.model]
|
|
503
|
+
if not model.check_access_rights("read", raise_exception=False):
|
|
504
|
+
model_description = (
|
|
505
|
+
self.env["ir.model"]._get(variable.model).display_name
|
|
506
|
+
)
|
|
507
|
+
raise ValidationError(
|
|
508
|
+
_("You can not select field of %(model)s.", model=model_description)
|
|
509
|
+
)
|
|
510
|
+
try:
|
|
511
|
+
variable._extract_value_from_field_path(model)
|
|
512
|
+
except UserError:
|
|
513
|
+
failing += variable
|
|
514
|
+
if failing:
|
|
515
|
+
model_description = (
|
|
516
|
+
self.env["ir.model"]._get(failing.mapped("model")[0]).display_name
|
|
517
|
+
)
|
|
518
|
+
raise ValidationError(
|
|
519
|
+
_(
|
|
520
|
+
"Variables %(field_names)s do not seem to be valid field path "
|
|
521
|
+
"for model %(model_name)s.",
|
|
522
|
+
field_names=", ".join(failing.mapped("field_name")),
|
|
523
|
+
model_name=model_description,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
@api.constrains("name")
|
|
528
|
+
def _check_name(self):
|
|
529
|
+
for variable in self:
|
|
530
|
+
if not variable._extract_variable_index():
|
|
531
|
+
raise ValidationError(
|
|
532
|
+
_(
|
|
533
|
+
"Template variable should be in format {{number}}. "
|
|
534
|
+
"Cannot parse '%(placeholder)s'",
|
|
535
|
+
placeholder=variable.name,
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
@api.depends("line_type", "name")
|
|
540
|
+
def _compute_display_name(self):
|
|
541
|
+
type_names = dict(self._fields["line_type"]._description_selection(self.env))
|
|
542
|
+
for variable in self:
|
|
543
|
+
type_name = type_names[variable.line_type or "body"]
|
|
544
|
+
variable.display_name = (
|
|
545
|
+
type_name
|
|
546
|
+
if variable.line_type == "header"
|
|
547
|
+
else f"{type_name} - {variable.name}"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
@api.onchange("model")
|
|
551
|
+
def _onchange_model_id(self):
|
|
552
|
+
self.field_name = False
|
|
553
|
+
|
|
554
|
+
def _get_variables_value(self, record):
|
|
555
|
+
value_by_name = {}
|
|
556
|
+
for variable in self:
|
|
557
|
+
value = variable._extract_value_from_field_path(record)
|
|
558
|
+
value_str = value and str(value) or ""
|
|
559
|
+
value_key = f"{variable.line_type}-{variable.name}"
|
|
560
|
+
value_by_name[value_key] = value_str
|
|
561
|
+
return value_by_name
|
|
562
|
+
|
|
563
|
+
def _extract_variable_index(self):
|
|
564
|
+
"""Extract variable index, located between '{{}}' markers."""
|
|
565
|
+
self.ensure_one()
|
|
566
|
+
try:
|
|
567
|
+
return int(self.name.replace("{{", "").replace("}}", ""))
|
|
568
|
+
except ValueError:
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
def _extract_value_from_field_path(self, record):
|
|
572
|
+
field_path = self.field_name
|
|
573
|
+
if not field_path:
|
|
574
|
+
return ""
|
|
575
|
+
try:
|
|
576
|
+
field_value = record.mapped(field_path)
|
|
577
|
+
except Exception as err:
|
|
578
|
+
raise UserError(
|
|
579
|
+
_(
|
|
580
|
+
"We were not able to fetch value of field: %(field)s",
|
|
581
|
+
field=field_path,
|
|
582
|
+
)
|
|
583
|
+
) from err
|
|
584
|
+
if isinstance(field_value, models.Model):
|
|
585
|
+
return " ".join((value.display_name or "") for value in field_value)
|
|
586
|
+
# find last field / last model when having chained fields
|
|
587
|
+
# e.g. 'partner_id.country_id.state' -> ['partner_id.country_id', 'state']
|
|
588
|
+
field_path_models = field_path.rsplit(".", 1)
|
|
589
|
+
if len(field_path_models) > 1:
|
|
590
|
+
last_model_path, last_fname = field_path_models
|
|
591
|
+
last_model = record.mapped(last_model_path)
|
|
592
|
+
else:
|
|
593
|
+
last_model, last_fname = record, field_path
|
|
594
|
+
last_field = last_model._fields[last_fname]
|
|
595
|
+
# return value instead of the key
|
|
596
|
+
if last_field.type == "selection":
|
|
597
|
+
return " ".join(
|
|
598
|
+
last_field.convert_to_export(value, last_model) for value in field_value
|
|
599
|
+
)
|
|
600
|
+
return " ".join(
|
|
601
|
+
str(value if value is not False and value is not None else "")
|
|
602
|
+
for value in field_value
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class MailWhatsAppTemplateButton(models.Model):
|
|
607
|
+
_name = "mail.whatsapp.template.button"
|
|
608
|
+
_description = "WhatsApp Template Button"
|
|
609
|
+
_order = "sequence,id"
|
|
610
|
+
|
|
611
|
+
sequence = fields.Integer()
|
|
612
|
+
name = fields.Char(string="Button Text", size=25)
|
|
613
|
+
template_id = fields.Many2one(
|
|
614
|
+
comodel_name="mail.whatsapp.template", required=True, ondelete="cascade"
|
|
615
|
+
)
|
|
616
|
+
button_type = fields.Selection(
|
|
617
|
+
[
|
|
618
|
+
("quick_reply", "Quick Reply"),
|
|
619
|
+
("url", "Visit Website"),
|
|
620
|
+
("phone_number", "Call Number"),
|
|
621
|
+
],
|
|
622
|
+
string="Type",
|
|
623
|
+
required=True,
|
|
624
|
+
default="quick_reply",
|
|
625
|
+
)
|
|
626
|
+
url_type = fields.Selection(
|
|
627
|
+
[("static", "Static"), ("dynamic", "Dynamic")],
|
|
628
|
+
default="static",
|
|
629
|
+
)
|
|
630
|
+
website_url = fields.Char()
|
|
631
|
+
call_number = fields.Char()
|
|
632
|
+
variable_ids = fields.One2many(
|
|
633
|
+
"mail.whatsapp.template.variable",
|
|
634
|
+
"button_id",
|
|
635
|
+
compute="_compute_variable_ids",
|
|
636
|
+
precompute=True,
|
|
637
|
+
store=True,
|
|
638
|
+
copy=True,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
_sql_constraints = [
|
|
642
|
+
(
|
|
643
|
+
"unique_name_per_template",
|
|
644
|
+
"UNIQUE(name, template_id)",
|
|
645
|
+
"Button name must be unique by template",
|
|
646
|
+
)
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
@api.constrains("button_type", "url_type", "website_url")
|
|
650
|
+
def _validate_website_url(self):
|
|
651
|
+
for button in self.filtered(lambda button: button.button_type == "url"):
|
|
652
|
+
parsed_url = urlparse(button.website_url)
|
|
653
|
+
if not (parsed_url.scheme in {"http", "https"} and parsed_url.netloc):
|
|
654
|
+
raise ValidationError(
|
|
655
|
+
_(
|
|
656
|
+
"Please enter a valid URL in the format 'https://www.example.com'."
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
@api.constrains("call_number")
|
|
661
|
+
def _validate_call_number(self):
|
|
662
|
+
for button in self:
|
|
663
|
+
if button.button_type == "phone_number":
|
|
664
|
+
phone_validation.phone_format(button.call_number, False, False)
|
|
665
|
+
|
|
666
|
+
@api.depends("button_type", "url_type", "website_url", "name")
|
|
667
|
+
def _compute_variable_ids(self):
|
|
668
|
+
dynamic_urls = self.filtered(
|
|
669
|
+
lambda button: button.button_type == "url" and button.url_type == "dynamic"
|
|
670
|
+
)
|
|
671
|
+
to_clear = self - dynamic_urls
|
|
672
|
+
for button in dynamic_urls:
|
|
673
|
+
if button.variable_ids:
|
|
674
|
+
button.variable_ids = [
|
|
675
|
+
(
|
|
676
|
+
1,
|
|
677
|
+
button.variable_ids[0].id,
|
|
678
|
+
{
|
|
679
|
+
"sample_value": button.website_url,
|
|
680
|
+
"line_type": "button",
|
|
681
|
+
"name": button.name,
|
|
682
|
+
"button_id": button.id,
|
|
683
|
+
"template_id": button.template_id.id,
|
|
684
|
+
},
|
|
685
|
+
),
|
|
686
|
+
]
|
|
687
|
+
else:
|
|
688
|
+
button.variable_ids = [
|
|
689
|
+
(
|
|
690
|
+
0,
|
|
691
|
+
0,
|
|
692
|
+
{
|
|
693
|
+
"sample_value": button.website_url,
|
|
694
|
+
"line_type": "button",
|
|
695
|
+
"name": button.name,
|
|
696
|
+
"button_id": button.id,
|
|
697
|
+
"template_id": button.template_id.id,
|
|
698
|
+
},
|
|
699
|
+
),
|
|
700
|
+
]
|
|
701
|
+
if to_clear:
|
|
702
|
+
to_clear.variable_ids = [(5, 0)]
|
|
703
|
+
|
|
704
|
+
def check_variable_ids(self):
|
|
705
|
+
for button in self:
|
|
706
|
+
if len(button.variable_ids) > 1:
|
|
707
|
+
raise ValidationError(_("Buttons may only contain one placeholder."))
|
|
708
|
+
if button.variable_ids and button.url_type != "dynamic":
|
|
709
|
+
raise ValidationError(_("Only dynamic urls may have a placeholder."))
|
|
710
|
+
elif button.url_type == "dynamic" and not button.variable_ids:
|
|
711
|
+
raise ValidationError(_("All dynamic urls must have a placeholder."))
|
|
712
|
+
if button.variable_ids.name != "{{1}}":
|
|
713
|
+
raise ValidationError(
|
|
714
|
+
_("The placeholder for a button can only be {{1}}.")
|
|
715
|
+
)
|
|
@@ -2,3 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|
|
2
2
|
access_whatsapp_composer,access.whatsapp.composer,model_whatsapp_composer,base.group_user,1,1,1,0
|
|
3
3
|
access_mail_whatsapp_template_group_system,mail_whatsapp_template_group_system,model_mail_whatsapp_template,base.group_system,1,1,1,1
|
|
4
4
|
access_mail_whatsapp_template_group_user,mail_whatsapp_template_group_user,model_mail_whatsapp_template,base.group_user,1,0,0,0
|
|
5
|
+
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
|
|
6
|
+
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
|
|
7
|
+
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
|
|
8
|
+
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
|
|
@@ -372,7 +372,7 @@ ul.auto-toc {
|
|
|
372
372
|
!! This file is generated by oca-gen-addon-readme !!
|
|
373
373
|
!! changes will be overwritten. !!
|
|
374
374
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
375
|
-
!! source digest: sha256:
|
|
375
|
+
!! source digest: sha256:2330f1752351311f792b205805bb8a22e000deb1c18c25801bb5675d1aef5f3f
|
|
376
376
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
|
377
377
|
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/social/tree/17.0/mail_gateway_whatsapp"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/social-17-0/social-17-0-mail_gateway_whatsapp"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
|
378
378
|
<p>This module allows to respond whatsapp chats.</p>
|