odoo-addon-mail-gateway-whatsapp 16.0.1.0.0.11__py3-none-any.whl → 16.0.1.1.0__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.
Files changed (30) hide show
  1. odoo/addons/mail_gateway_whatsapp/README.rst +13 -1
  2. odoo/addons/mail_gateway_whatsapp/__init__.py +1 -0
  3. odoo/addons/mail_gateway_whatsapp/__manifest__.py +4 -1
  4. odoo/addons/mail_gateway_whatsapp/i18n/mail_gateway_whatsapp.pot +590 -0
  5. odoo/addons/mail_gateway_whatsapp/models/__init__.py +1 -0
  6. odoo/addons/mail_gateway_whatsapp/models/mail_gateway.py +61 -1
  7. odoo/addons/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py +24 -3
  8. odoo/addons/mail_gateway_whatsapp/models/mail_whatsapp_template.py +193 -0
  9. odoo/addons/mail_gateway_whatsapp/readme/CONFIGURE.rst +1 -0
  10. odoo/addons/mail_gateway_whatsapp/readme/CONTRIBUTORS.rst +3 -0
  11. odoo/addons/mail_gateway_whatsapp/readme/ROADMAP.rst +4 -0
  12. odoo/addons/mail_gateway_whatsapp/security/ir.model.access.csv +2 -0
  13. odoo/addons/mail_gateway_whatsapp/security/security.xml +8 -0
  14. odoo/addons/mail_gateway_whatsapp/static/description/index.html +41 -19
  15. odoo/addons/mail_gateway_whatsapp/tests/__init__.py +1 -0
  16. odoo/addons/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py +75 -1
  17. odoo/addons/mail_gateway_whatsapp/tests/test_mail_whatsapp_template.py +170 -0
  18. odoo/addons/mail_gateway_whatsapp/tools/__init__.py +1 -0
  19. odoo/addons/mail_gateway_whatsapp/tools/const.py +75 -0
  20. odoo/addons/mail_gateway_whatsapp/views/mail_gateway.xml +28 -0
  21. odoo/addons/mail_gateway_whatsapp/views/mail_whatsapp_template_views.xml +154 -0
  22. odoo/addons/mail_gateway_whatsapp/wizards/__init__.py +1 -0
  23. odoo/addons/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.py +30 -0
  24. odoo/addons/mail_gateway_whatsapp/wizards/mail_compose_gateway_message.xml +31 -0
  25. odoo/addons/mail_gateway_whatsapp/wizards/whatsapp_composer.py +53 -1
  26. odoo/addons/mail_gateway_whatsapp/wizards/whatsapp_composer.xml +11 -1
  27. {odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.11.dist-info → odoo_addon_mail_gateway_whatsapp-16.0.1.1.0.dist-info}/METADATA +14 -2
  28. {odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.11.dist-info → odoo_addon_mail_gateway_whatsapp-16.0.1.1.0.dist-info}/RECORD +30 -21
  29. {odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.11.dist-info → odoo_addon_mail_gateway_whatsapp-16.0.1.1.0.dist-info}/WHEEL +1 -1
  30. {odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.11.dist-info → odoo_addon_mail_gateway_whatsapp-16.0.1.1.0.dist-info}/top_level.txt +0 -0
@@ -3,3 +3,4 @@ from . import mail_thread
3
3
  from . import mail_gateway_whatsapp
4
4
  from . import mail_channel
5
5
  from . import res_partner
6
+ from . import mail_whatsapp_template
@@ -1,7 +1,12 @@
1
1
  # Copyright 2022 Creu Blanca
2
2
  # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3
+ import requests
4
+ from werkzeug.urls import url_join
3
5
 
4
- from odoo import fields, models
6
+ from odoo import _, api, fields, models
7
+ from odoo.exceptions import UserError
8
+
9
+ BASE_URL = "https://graph.facebook.com/"
5
10
 
6
11
 
7
12
  class MailGateway(models.Model):
@@ -13,3 +18,58 @@ class MailGateway(models.Model):
13
18
  )
14
19
  whatsapp_from_phone = fields.Char()
15
20
  whatsapp_version = fields.Char(default="15.0")
21
+ whatsapp_account_id = fields.Char()
22
+ whatsapp_template_ids = fields.One2many("mail.whatsapp.template", "gateway_id")
23
+ whatsapp_template_count = fields.Integer(compute="_compute_whatsapp_template_count")
24
+
25
+ @api.depends("whatsapp_template_ids")
26
+ def _compute_whatsapp_template_count(self):
27
+ for gateway in self:
28
+ gateway.whatsapp_template_count = len(gateway.whatsapp_template_ids)
29
+
30
+ def button_import_whatsapp_template(self):
31
+ self.ensure_one()
32
+ WhatsappTemplate = self.env["mail.whatsapp.template"]
33
+ if not self.whatsapp_account_id:
34
+ raise UserError(_("WhatsApp Account is required to import templates."))
35
+ meta_info = {}
36
+ template_url = url_join(
37
+ BASE_URL,
38
+ f"v{self.whatsapp_version}/{self.whatsapp_account_id}/message_templates",
39
+ )
40
+ try:
41
+ meta_request = requests.get(
42
+ template_url,
43
+ headers={"Authorization": f"Bearer {self.token}"},
44
+ timeout=10,
45
+ )
46
+ meta_request.raise_for_status()
47
+ meta_info = meta_request.json()
48
+ except Exception as err:
49
+ raise UserError(str(err)) from err
50
+ current_templates = WhatsappTemplate.with_context(active_test=False).search(
51
+ [("gateway_id", "=", self.id)]
52
+ )
53
+ templates_by_id = {t.template_uid: t for t in current_templates}
54
+ create_vals = []
55
+ for template_data in meta_info.get("data", []):
56
+ ws_template = templates_by_id.get(template_data["id"])
57
+ if ws_template:
58
+ ws_template.write(
59
+ WhatsappTemplate._prepare_values_to_import(self, template_data)
60
+ )
61
+ else:
62
+ create_vals.append(
63
+ WhatsappTemplate._prepare_values_to_import(self, template_data)
64
+ )
65
+ WhatsappTemplate.create(create_vals)
66
+ return {
67
+ "type": "ir.actions.client",
68
+ "tag": "display_notification",
69
+ "params": {
70
+ "title": _("WathsApp Templates"),
71
+ "type": "success",
72
+ "message": _("Synchronization successfully."),
73
+ "next": {"type": "ir.actions.act_window_close"},
74
+ },
75
+ }
@@ -285,14 +285,35 @@ class MailGatewayWhatsappService(models.AbstractModel):
285
285
  def _send_payload(
286
286
  self, channel, body=False, media_id=False, media_type=False, media_name=False
287
287
  ):
288
+ whatsapp_template = self.env["mail.whatsapp.template"]
289
+ if self.env.context.get("whatsapp_template_id"):
290
+ whatsapp_template = self.env["mail.whatsapp.template"].browse(
291
+ self.env.context.get("whatsapp_template_id")
292
+ )
288
293
  if body:
289
- return {
294
+ payload = {
290
295
  "messaging_product": "whatsapp",
291
296
  "recipient_type": "individual",
292
297
  "to": channel.gateway_channel_token,
293
- "type": "text",
294
- "text": {"preview_url": False, "body": html2plaintext(body)},
295
298
  }
299
+ if whatsapp_template:
300
+ payload.update(
301
+ {
302
+ "type": "template",
303
+ "template": {
304
+ "name": whatsapp_template.template_name,
305
+ "language": {"code": whatsapp_template.language},
306
+ },
307
+ }
308
+ )
309
+ else:
310
+ payload.update(
311
+ {
312
+ "type": "text",
313
+ "text": {"preview_url": False, "body": html2plaintext(body)},
314
+ }
315
+ )
316
+ return payload
296
317
  if media_id:
297
318
  media_data = {"id": media_id}
298
319
  if media_type == "document":
@@ -0,0 +1,193 @@
1
+ # Copyright 2024 Tecnativa - Carlos López
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+ import re
4
+
5
+ import requests
6
+ from werkzeug.urls import url_join
7
+
8
+ from odoo import api, fields, models
9
+ from odoo.exceptions import UserError
10
+ from odoo.tools import ustr
11
+
12
+ from odoo.addons.http_routing.models.ir_http import slugify
13
+
14
+ from ..tools.const import supported_languages
15
+ from .mail_gateway import BASE_URL
16
+
17
+
18
+ class MailWhatsAppTemplate(models.Model):
19
+ _name = "mail.whatsapp.template"
20
+ _description = "Mail WhatsApp template"
21
+
22
+ name = fields.Char(required=True)
23
+ body = fields.Text(required=True)
24
+ header = fields.Char()
25
+ footer = fields.Char()
26
+ template_name = fields.Char(
27
+ compute="_compute_template_name", store=True, copy=False
28
+ )
29
+ is_supported = fields.Boolean(copy=False)
30
+ template_uid = fields.Char(readonly=True, copy=False)
31
+ category = fields.Selection(
32
+ [
33
+ ("authentication", "Authentication"),
34
+ ("marketing", "Marketing"),
35
+ ("utility", "Utility"),
36
+ ],
37
+ required=True,
38
+ )
39
+ state = fields.Selection(
40
+ [
41
+ ("draft", "Draft"),
42
+ ("pending", "Pending"),
43
+ ("approved", "Approved"),
44
+ ("in_appeal", "In Appeal"),
45
+ ("rejected", "Rejected"),
46
+ ("pending_deletion", "Pending Deletion"),
47
+ ("deleted", "Deleted"),
48
+ ("disabled", "Disabled"),
49
+ ("paused", "Paused"),
50
+ ("limit_exceeded", "Limit Exceeded"),
51
+ ("archived", "Archived"),
52
+ ],
53
+ default="draft",
54
+ required=True,
55
+ )
56
+ language = fields.Selection(supported_languages, required=True)
57
+ gateway_id = fields.Many2one(
58
+ "mail.gateway",
59
+ domain=[("gateway_type", "=", "whatsapp")],
60
+ required=True,
61
+ ondelete="cascade",
62
+ )
63
+ company_id = fields.Many2one(
64
+ "res.company", related="gateway_id.company_id", store=True
65
+ )
66
+
67
+ _sql_constraints = [
68
+ (
69
+ "unique_name_gateway_id",
70
+ "unique(name, language, gateway_id)",
71
+ "Duplicate name is not allowed for Gateway.",
72
+ )
73
+ ]
74
+
75
+ @api.depends("name", "state", "template_uid")
76
+ def _compute_template_name(self):
77
+ for template in self:
78
+ if not template.template_name or (
79
+ template.state == "draft" and not template.template_uid
80
+ ):
81
+ template.template_name = re.sub(
82
+ r"\W+", "_", slugify(template.name or "")
83
+ )
84
+
85
+ def button_back2draft(self):
86
+ self.write({"state": "draft"})
87
+
88
+ def button_export_template(self):
89
+ self.ensure_one()
90
+ gateway = self.gateway_id
91
+ template_url = url_join(
92
+ BASE_URL,
93
+ f"v{gateway.whatsapp_version}/{gateway.whatsapp_account_id}/message_templates",
94
+ )
95
+ try:
96
+ payload = self._prepare_values_to_export()
97
+ response = requests.post(
98
+ template_url,
99
+ headers={"Authorization": "Bearer %s" % gateway.token},
100
+ json=payload,
101
+ timeout=10,
102
+ )
103
+ response.raise_for_status()
104
+ json_data = response.json()
105
+ self.write(
106
+ {
107
+ "template_uid": json_data.get("id"),
108
+ "state": json_data.get("status").lower(),
109
+ "is_supported": True,
110
+ }
111
+ )
112
+ except requests.exceptions.HTTPError as ex:
113
+ msj = f"{ustr(ex)} \n{ex.response.text}"
114
+ raise UserError(msj) from ex
115
+ except Exception as err:
116
+ raise UserError(ustr(err)) from err
117
+
118
+ def _prepare_values_to_export(self):
119
+ components = self._prepare_components_to_export()
120
+ return {
121
+ "name": self.template_name,
122
+ "category": self.category.upper(),
123
+ "language": self.language,
124
+ "components": components,
125
+ }
126
+
127
+ def _prepare_components_to_export(self):
128
+ components = [{"type": "BODY", "text": self.body}]
129
+ if self.header:
130
+ components.append(
131
+ {
132
+ "type": "HEADER",
133
+ "format": "text",
134
+ "text": self.header,
135
+ }
136
+ )
137
+ if self.footer:
138
+ components.append(
139
+ {
140
+ "type": "FOOTER",
141
+ "text": self.footer,
142
+ }
143
+ )
144
+ # TODO: add more components(buttons, location, etc)
145
+ return components
146
+
147
+ def button_sync_template(self):
148
+ self.ensure_one()
149
+ gateway = self.gateway_id
150
+ template_url = url_join(
151
+ BASE_URL,
152
+ f"{self.template_uid}",
153
+ )
154
+ try:
155
+ response = requests.get(
156
+ template_url,
157
+ headers={"Authorization": "Bearer %s" % gateway.token},
158
+ timeout=10,
159
+ )
160
+ response.raise_for_status()
161
+ json_data = response.json()
162
+ vals = self._prepare_values_to_import(gateway, json_data)
163
+ self.write(vals)
164
+ except Exception as err:
165
+ raise UserError(str(err)) from err
166
+ return {
167
+ "type": "ir.actions.client",
168
+ "tag": "reload",
169
+ }
170
+
171
+ @api.model
172
+ def _prepare_values_to_import(self, gateway, json_data):
173
+ vals = {
174
+ "name": json_data.get("name").replace("_", " ").title(),
175
+ "template_name": json_data.get("name"),
176
+ "category": json_data.get("category").lower(),
177
+ "language": json_data.get("language"),
178
+ "state": json_data.get("status").lower(),
179
+ "template_uid": json_data.get("id"),
180
+ "gateway_id": gateway.id,
181
+ }
182
+ is_supported = True
183
+ for component in json_data.get("components", []):
184
+ if component["type"] == "HEADER" and component["format"] == "TEXT":
185
+ vals["header"] = component["text"]
186
+ elif component["type"] == "BODY":
187
+ vals["body"] = component["text"]
188
+ elif component["type"] == "FOOTER":
189
+ vals["footer"] = component["text"]
190
+ else:
191
+ is_supported = False
192
+ vals["is_supported"] = is_supported
193
+ return vals
@@ -19,6 +19,7 @@ In order to make it you must follow this steps:
19
19
 
20
20
  * Use the Meta App authentication key as `Token` field
21
21
  * Use the Meta App Phone Number ID as `Whatsapp from Phone` field
22
+ * Use the Meta Account Business ID as `Whatsapp account` field (only if you need sync templates)
22
23
  * Write your own `Webhook key`
23
24
  * Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
24
25
  * Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
@@ -1,2 +1,5 @@
1
1
  * Olga Marco <olga.marco@creublanca.es>
2
2
  * Enric Tobella <etobella@creublanca.es>
3
+ * `Tecnativa <https://www.tecnativa.com>`_:
4
+
5
+ * Carlos Lopez
@@ -0,0 +1,4 @@
1
+ **WhatsApp templates**
2
+
3
+ * Add support for `Variables`
4
+ * Add support for `Buttons`
@@ -1,2 +1,4 @@
1
1
  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
+ access_mail_whatsapp_template_group_system,mail_whatsapp_template_group_system,model_mail_whatsapp_template,base.group_system,1,1,1,1
4
+ access_mail_whatsapp_template_group_user,mail_whatsapp_template_group_user,model_mail_whatsapp_template,base.group_user,1,0,0,0
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?>
2
+ <odoo noupdate="1">
3
+ <record id="mail_whatsapp_template_rule" model="ir.rule">
4
+ <field name="name">WhatsApp template: multicompany</field>
5
+ <field name="model_id" ref="model_mail_whatsapp_template" />
6
+ <field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
7
+ </record>
8
+ </odoo>
@@ -8,10 +8,11 @@
8
8
 
9
9
  /*
10
10
  :Author: David Goodger (goodger@python.org)
11
- :Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
11
+ :Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
12
12
  :Copyright: This stylesheet has been placed in the public domain.
13
13
 
14
14
  Default cascading style sheet for the HTML output of Docutils.
15
+ Despite the name, some widely supported CSS2 features are used.
15
16
 
16
17
  See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
17
18
  customize this style sheet.
@@ -274,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
274
275
  margin-left: 2em ;
275
276
  margin-right: 2em }
276
277
 
277
- pre.code .ln { color: grey; } /* line numbers */
278
+ pre.code .ln { color: gray; } /* line numbers */
278
279
  pre.code, code { background-color: #eeeeee }
279
280
  pre.code .comment, code .comment { color: #5C6576 }
280
281
  pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -300,7 +301,7 @@ span.option {
300
301
  span.pre {
301
302
  white-space: pre }
302
303
 
303
- span.problematic {
304
+ span.problematic, pre.problematic {
304
305
  color: red }
305
306
 
306
307
  span.section-subtitle {
@@ -366,7 +367,7 @@ ul.auto-toc {
366
367
  !! This file is generated by oca-gen-addon-readme !!
367
368
  !! changes will be overwritten. !!
368
369
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
369
- !! source digest: sha256:7e8914aa79754fb57cdfe774336f8ffe4c6784994839b64e09cf393ed7c2aa4a
370
+ !! source digest: sha256:70475641a2c7498a1ee67811eadb397e33c33435cdd2efb8edce729026ad09c4
370
371
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
371
372
  <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/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/social/tree/16.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-16-0/social-16-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&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
372
373
  <p>This module allows to respond whatsapp chats.</p>
@@ -381,12 +382,13 @@ of partners in an integrated way.</p>
381
382
  </ul>
382
383
  </li>
383
384
  <li><a class="reference internal" href="#usage" id="toc-entry-4">Usage</a></li>
384
- <li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
385
- <li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
386
- <li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
387
- <li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
388
- <li><a class="reference internal" href="#other-credits" id="toc-entry-9">Other credits</a></li>
389
- <li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
385
+ <li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-5">Known issues / Roadmap</a></li>
386
+ <li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
387
+ <li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
388
+ <li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
389
+ <li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
390
+ <li><a class="reference internal" href="#other-credits" id="toc-entry-10">Other credits</a></li>
391
+ <li><a class="reference internal" href="#maintainers" id="toc-entry-11">Maintainers</a></li>
390
392
  </ul>
391
393
  </li>
392
394
  </ul>
@@ -412,6 +414,7 @@ In order to make it you must follow this steps:</p>
412
414
  <ul class="simple">
413
415
  <li>Use the Meta App authentication key as <cite>Token</cite> field</li>
414
416
  <li>Use the Meta App Phone Number ID as <cite>Whatsapp from Phone</cite> field</li>
417
+ <li>Use the Meta Account Business ID as <cite>Whatsapp account</cite> field (only if you need sync templates)</li>
415
418
  <li>Write your own <cite>Webhook key</cite></li>
416
419
  <li>Use the Application Secret Key on <cite>Whatsapp Security Key</cite>. It will be used in order to validate the data</li>
417
420
  <li>Press the <cite>Integrate Webhook Key</cite>. In this case, it will not integrate it, we need to make it manually</li>
@@ -434,8 +437,16 @@ In order to make it you must follow this steps:</p>
434
437
  <li>Now you will be able to respond and receive messages to this person.</li>
435
438
  </ol>
436
439
  </div>
440
+ <div class="section" id="known-issues-roadmap">
441
+ <h1><a class="toc-backref" href="#toc-entry-5">Known issues / Roadmap</a></h1>
442
+ <p><strong>WhatsApp templates</strong></p>
443
+ <ul class="simple">
444
+ <li>Add support for <cite>Variables</cite></li>
445
+ <li>Add support for <cite>Buttons</cite></li>
446
+ </ul>
447
+ </div>
437
448
  <div class="section" id="bug-tracker">
438
- <h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
449
+ <h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
439
450
  <p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
440
451
  In case of trouble, please check there if your issue has already been reported.
441
452
  If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -443,29 +454,40 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
443
454
  <p>Do not contact contributors directly about support or help with technical issues.</p>
444
455
  </div>
445
456
  <div class="section" id="credits">
446
- <h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
457
+ <h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
447
458
  <div class="section" id="authors">
448
- <h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
459
+ <h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
449
460
  <ul class="simple">
450
461
  <li>Creu Blanca</li>
451
462
  <li>Dixmit</li>
452
463
  </ul>
453
464
  </div>
454
465
  <div class="section" id="contributors">
455
- <h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
466
+ <h2><a class="toc-backref" href="#toc-entry-9">Contributors</a></h2>
467
+ <ul>
468
+ <li><p class="first">Olga Marco &lt;<a class="reference external" href="mailto:olga.marco&#64;creublanca.es">olga.marco&#64;creublanca.es</a>&gt;</p>
469
+ </li>
470
+ <li><p class="first">Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</p>
471
+ </li>
472
+ <li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
473
+ <blockquote>
456
474
  <ul class="simple">
457
- <li>Olga Marco &lt;<a class="reference external" href="mailto:olga.marco&#64;creublanca.es">olga.marco&#64;creublanca.es</a>&gt;</li>
458
- <li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
475
+ <li>Carlos Lopez</li>
476
+ </ul>
477
+ </blockquote>
478
+ </li>
459
479
  </ul>
460
480
  </div>
461
481
  <div class="section" id="other-credits">
462
- <h2><a class="toc-backref" href="#toc-entry-9">Other credits</a></h2>
482
+ <h2><a class="toc-backref" href="#toc-entry-10">Other credits</a></h2>
463
483
  <p>This work has been funded by AEOdoo (Asociación Española de Odoo - <a class="reference external" href="https://www.aeodoo.org">https://www.aeodoo.org</a>)</p>
464
484
  </div>
465
485
  <div class="section" id="maintainers">
466
- <h2><a class="toc-backref" href="#toc-entry-10">Maintainers</a></h2>
486
+ <h2><a class="toc-backref" href="#toc-entry-11">Maintainers</a></h2>
467
487
  <p>This module is maintained by the OCA.</p>
468
- <a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
488
+ <a class="reference external image-reference" href="https://odoo-community.org">
489
+ <img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
490
+ </a>
469
491
  <p>OCA, or the Odoo Community Association, is a nonprofit organization whose
470
492
  mission is to support the collaborative development of Odoo features and
471
493
  promote its widespread use.</p>
@@ -1 +1,2 @@
1
1
  from . import test_mail_gateway_whatsapp
2
+ from . import test_mail_whatsapp_template
@@ -6,7 +6,10 @@ import hmac
6
6
  import json
7
7
  from unittest.mock import MagicMock, patch
8
8
 
9
+ from markupsafe import Markup
10
+
9
11
  from odoo.exceptions import UserError
12
+ from odoo.tests import Form, RecordCapturer
10
13
  from odoo.tests.common import tagged
11
14
  from odoo.tools import mute_logger
12
15
 
@@ -14,7 +17,7 @@ from odoo.addons.mail_gateway.tests.common import MailGatewayTestCase
14
17
 
15
18
 
16
19
  @tagged("-at_install", "post_install")
17
- class TestMailGatewayTelegram(MailGatewayTestCase):
20
+ class TestMailGatewayWhatsApp(MailGatewayTestCase):
18
21
  @classmethod
19
22
  def setUpClass(cls):
20
23
  super().setUpClass()
@@ -26,6 +29,18 @@ class TestMailGatewayTelegram(MailGatewayTestCase):
26
29
  "token": "token",
27
30
  "whatsapp_security_key": "key",
28
31
  "webhook_secret": "MY-SECRET",
32
+ "member_ids": [(4, cls.env.user.id)],
33
+ }
34
+ )
35
+ cls.ws_template = cls.env["mail.whatsapp.template"].create(
36
+ {
37
+ "name": "New template",
38
+ "category": "marketing",
39
+ "language": "es",
40
+ "body": "Demo template",
41
+ "state": "approved",
42
+ "is_supported": True,
43
+ "gateway_id": cls.gateway.id,
29
44
  }
30
45
  )
31
46
  cls.partner = cls.env["res.partner"].create(
@@ -279,6 +294,65 @@ class TestMailGatewayTelegram(MailGatewayTestCase):
279
294
  )
280
295
  self.assertEqual(message.notification_ids.notification_status, "exception")
281
296
 
297
+ def test_send_message_text(self):
298
+ """
299
+ Test that the message is sent correctly
300
+ - First message need a template
301
+ - Second message does not need a template
302
+ """
303
+ ctx = {
304
+ "default_res_model": self.partner._name,
305
+ "default_res_id": self.partner.id,
306
+ "default_number_field_name": "mobile",
307
+ "default_composition_mode": "comment",
308
+ "default_gateway_id": self.gateway.id,
309
+ }
310
+ self.gateway.whatsapp_account_id = "123456"
311
+ form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx))
312
+ form_composer.body = "Body test"
313
+ self.assertTrue(form_composer.is_required_template)
314
+ self.assertTrue(form_composer._get_modifier("template_id", "required"))
315
+ form_composer.template_id = self.ws_template
316
+ composer = form_composer.save()
317
+ self.assertEqual(composer.body, "Demo template")
318
+ channel = self.partner._whatsapp_get_channel(
319
+ composer.number_field_name, composer.gateway_id
320
+ )
321
+ message_domain = [
322
+ ("gateway_type", "=", "whatsapp"),
323
+ ("model", "=", channel._name),
324
+ ("res_id", "=", channel.id),
325
+ ]
326
+ with RecordCapturer(self.env["mail.message"], message_domain) as capture, patch(
327
+ "requests.post"
328
+ ) as post_mock:
329
+ post_mock.return_value = MagicMock()
330
+ composer.action_send_whatsapp()
331
+ self.assertEqual(len(capture.records), 1)
332
+ self.assertEqual(capture.records.body, Markup("<p>Demo template</p>"))
333
+ # second message
334
+ form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx))
335
+ form_composer.body = "Body test"
336
+ self.assertFalse(form_composer.is_required_template)
337
+ self.assertFalse(form_composer._get_modifier("template_id", "required"))
338
+ composer = form_composer.save()
339
+ self.assertEqual(composer.body, "Body test")
340
+ channel = self.partner._whatsapp_get_channel(
341
+ composer.number_field_name, composer.gateway_id
342
+ )
343
+ message_domain = [
344
+ ("gateway_type", "=", "whatsapp"),
345
+ ("model", "=", channel._name),
346
+ ("res_id", "=", channel.id),
347
+ ]
348
+ with RecordCapturer(self.env["mail.message"], message_domain) as capture, patch(
349
+ "requests.post"
350
+ ) as post_mock:
351
+ post_mock.return_value = MagicMock()
352
+ composer.action_send_whatsapp()
353
+ self.assertEqual(len(capture.records), 1)
354
+ self.assertEqual(capture.records.body, Markup("<p>Body test</p>"))
355
+
282
356
  def test_compose(self):
283
357
  self.gateway.webhook_key = self.webhook
284
358
  self.gateway.set_webhook()