mirror of https://github.com/OCA/social.git
[IMP] mail_gateway_whatsapp: Add support for WhatsApp templates
If the company wants to initiate a conversation with a customer, a template must be used; otherwise, messages will not be sent. This also applies when the last conversation with the customer is older than 24 hours. - Enabled downloading templates from META - Added the ability to create templates directly in Odoo. Note: Currently, templates with variables or buttons are not supported.pull/1497/head
parent
a4eeca52d3
commit
25a1264c6e
|
@ -62,6 +62,7 @@ In order to make it you must follow this steps:
|
|||
|
||||
* Use the Meta App authentication key as `Token` field
|
||||
* Use the Meta App Phone Number ID as `Whatsapp from Phone` field
|
||||
* Use the Meta Account Business ID as `Whatsapp account` field (only if you need sync templates)
|
||||
* Write your own `Webhook key`
|
||||
* Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
|
||||
* Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
|
||||
|
@ -79,6 +80,14 @@ Usage
|
|||
2. Wait until someone starts a conversation.
|
||||
3. Now you will be able to respond and receive messages to this person.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
**WhatsApp templates**
|
||||
|
||||
* Add support for `Variables`
|
||||
* Add support for `Buttons`
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
|
@ -103,6 +112,9 @@ Contributors
|
|||
|
||||
* Olga Marco <olga.marco@creublanca.es>
|
||||
* Enric Tobella <etobella@creublanca.es>
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* Carlos Lopez
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from . import models
|
||||
from . import tools
|
||||
|
||||
# from . import services
|
||||
from . import wizards
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
"depends": ["mail_gateway", "phone_validation"],
|
||||
"external_dependencies": {"python": ["requests_toolbelt"]},
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"wizards/whatsapp_composer.xml",
|
||||
"wizards/mail_compose_gateway_message.xml",
|
||||
"views/mail_whatsapp_template_views.xml",
|
||||
"views/mail_gateway.xml",
|
||||
],
|
||||
"assets": {
|
||||
|
|
|
@ -3,3 +3,4 @@ from . import mail_thread
|
|||
from . import mail_gateway_whatsapp
|
||||
from . import mail_channel
|
||||
from . import res_partner
|
||||
from . import mail_whatsapp_template
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
# Copyright 2022 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import requests
|
||||
from werkzeug.urls import url_join
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
BASE_URL = "https://graph.facebook.com/"
|
||||
|
||||
|
||||
class MailGateway(models.Model):
|
||||
|
@ -13,3 +18,58 @@ class MailGateway(models.Model):
|
|||
)
|
||||
whatsapp_from_phone = fields.Char()
|
||||
whatsapp_version = fields.Char(default="15.0")
|
||||
whatsapp_account_id = fields.Char()
|
||||
whatsapp_template_ids = fields.One2many("mail.whatsapp.template", "gateway_id")
|
||||
whatsapp_template_count = fields.Integer(compute="_compute_whatsapp_template_count")
|
||||
|
||||
@api.depends("whatsapp_template_ids")
|
||||
def _compute_whatsapp_template_count(self):
|
||||
for gateway in self:
|
||||
gateway.whatsapp_template_count = len(gateway.whatsapp_template_ids)
|
||||
|
||||
def button_import_whatsapp_template(self):
|
||||
self.ensure_one()
|
||||
WhatsappTemplate = self.env["mail.whatsapp.template"]
|
||||
if not self.whatsapp_account_id:
|
||||
raise UserError(_("WhatsApp Account is required to import templates."))
|
||||
meta_info = {}
|
||||
template_url = url_join(
|
||||
BASE_URL,
|
||||
f"v{self.whatsapp_version}/{self.whatsapp_account_id}/message_templates",
|
||||
)
|
||||
try:
|
||||
meta_request = requests.get(
|
||||
template_url,
|
||||
headers={"Authorization": f"Bearer {self.token}"},
|
||||
timeout=10,
|
||||
)
|
||||
meta_request.raise_for_status()
|
||||
meta_info = meta_request.json()
|
||||
except Exception as err:
|
||||
raise UserError(str(err)) from err
|
||||
current_templates = WhatsappTemplate.with_context(active_test=False).search(
|
||||
[("gateway_id", "=", self.id)]
|
||||
)
|
||||
templates_by_id = {t.template_uid: t for t in current_templates}
|
||||
create_vals = []
|
||||
for template_data in meta_info.get("data", []):
|
||||
ws_template = templates_by_id.get(template_data["id"])
|
||||
if ws_template:
|
||||
ws_template.write(
|
||||
WhatsappTemplate._prepare_values_to_import(self, template_data)
|
||||
)
|
||||
else:
|
||||
create_vals.append(
|
||||
WhatsappTemplate._prepare_values_to_import(self, template_data)
|
||||
)
|
||||
WhatsappTemplate.create(create_vals)
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("WathsApp Templates"),
|
||||
"type": "success",
|
||||
"message": _("Synchronization successfully."),
|
||||
"next": {"type": "ir.actions.act_window_close"},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -285,14 +285,35 @@ class MailGatewayWhatsappService(models.AbstractModel):
|
|||
def _send_payload(
|
||||
self, channel, body=False, media_id=False, media_type=False, media_name=False
|
||||
):
|
||||
whatsapp_template = self.env["mail.whatsapp.template"]
|
||||
if self.env.context.get("whatsapp_template_id"):
|
||||
whatsapp_template = self.env["mail.whatsapp.template"].browse(
|
||||
self.env.context.get("whatsapp_template_id")
|
||||
)
|
||||
if body:
|
||||
return {
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"recipient_type": "individual",
|
||||
"to": channel.gateway_channel_token,
|
||||
"type": "text",
|
||||
"text": {"preview_url": False, "body": html2plaintext(body)},
|
||||
}
|
||||
if whatsapp_template:
|
||||
payload.update(
|
||||
{
|
||||
"type": "template",
|
||||
"template": {
|
||||
"name": whatsapp_template.template_name,
|
||||
"language": {"code": whatsapp_template.language},
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
payload.update(
|
||||
{
|
||||
"type": "text",
|
||||
"text": {"preview_url": False, "body": html2plaintext(body)},
|
||||
}
|
||||
)
|
||||
return payload
|
||||
if media_id:
|
||||
media_data = {"id": media_id}
|
||||
if media_type == "document":
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
# Copyright 2024 Tecnativa - Carlos López
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
import re
|
||||
|
||||
import requests
|
||||
from werkzeug.urls import url_join
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import ustr
|
||||
|
||||
from odoo.addons.http_routing.models.ir_http import slugify
|
||||
|
||||
from ..tools.const import supported_languages
|
||||
from .mail_gateway import BASE_URL
|
||||
|
||||
|
||||
class MailWhatsAppTemplate(models.Model):
|
||||
_name = "mail.whatsapp.template"
|
||||
_description = "Mail WhatsApp template"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
body = fields.Text(required=True)
|
||||
header = fields.Char()
|
||||
footer = fields.Char()
|
||||
template_name = fields.Char(
|
||||
compute="_compute_template_name", store=True, copy=False
|
||||
)
|
||||
is_supported = fields.Boolean(copy=False)
|
||||
template_uid = fields.Char(readonly=True, copy=False)
|
||||
category = fields.Selection(
|
||||
[
|
||||
("authentication", "Authentication"),
|
||||
("marketing", "Marketing"),
|
||||
("utility", "Utility"),
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("pending", "Pending"),
|
||||
("approved", "Approved"),
|
||||
("in_appeal", "In Appeal"),
|
||||
("rejected", "Rejected"),
|
||||
("pending_deletion", "Pending Deletion"),
|
||||
("deleted", "Deleted"),
|
||||
("disabled", "Disabled"),
|
||||
("paused", "Paused"),
|
||||
("limit_exceeded", "Limit Exceeded"),
|
||||
("archived", "Archived"),
|
||||
],
|
||||
default="draft",
|
||||
required=True,
|
||||
)
|
||||
language = fields.Selection(supported_languages, required=True)
|
||||
gateway_id = fields.Many2one(
|
||||
"mail.gateway",
|
||||
domain=[("gateway_type", "=", "whatsapp")],
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", related="gateway_id.company_id", store=True
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"unique_name_gateway_id",
|
||||
"unique(name, language, gateway_id)",
|
||||
"Duplicate name is not allowed for Gateway.",
|
||||
)
|
||||
]
|
||||
|
||||
@api.depends("name", "state", "template_uid")
|
||||
def _compute_template_name(self):
|
||||
for template in self:
|
||||
if not template.template_name or (
|
||||
template.state == "draft" and not template.template_uid
|
||||
):
|
||||
template.template_name = re.sub(
|
||||
r"\W+", "_", slugify(template.name or "")
|
||||
)
|
||||
|
||||
def button_back2draft(self):
|
||||
self.write({"state": "draft"})
|
||||
|
||||
def button_export_template(self):
|
||||
self.ensure_one()
|
||||
gateway = self.gateway_id
|
||||
template_url = url_join(
|
||||
BASE_URL,
|
||||
f"v{gateway.whatsapp_version}/{gateway.whatsapp_account_id}/message_templates",
|
||||
)
|
||||
try:
|
||||
payload = self._prepare_values_to_export()
|
||||
response = requests.post(
|
||||
template_url,
|
||||
headers={"Authorization": "Bearer %s" % gateway.token},
|
||||
json=payload,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
json_data = response.json()
|
||||
self.write(
|
||||
{
|
||||
"template_uid": json_data.get("id"),
|
||||
"state": json_data.get("status").lower(),
|
||||
"is_supported": True,
|
||||
}
|
||||
)
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
msj = f"{ustr(ex)} \n{ex.response.text}"
|
||||
raise UserError(msj) from ex
|
||||
except Exception as err:
|
||||
raise UserError(ustr(err)) from err
|
||||
|
||||
def _prepare_values_to_export(self):
|
||||
components = self._prepare_components_to_export()
|
||||
return {
|
||||
"name": self.template_name,
|
||||
"category": self.category.upper(),
|
||||
"language": self.language,
|
||||
"components": components,
|
||||
}
|
||||
|
||||
def _prepare_components_to_export(self):
|
||||
components = [{"type": "BODY", "text": self.body}]
|
||||
if self.header:
|
||||
components.append(
|
||||
{
|
||||
"type": "HEADER",
|
||||
"format": "text",
|
||||
"text": self.header,
|
||||
}
|
||||
)
|
||||
if self.footer:
|
||||
components.append(
|
||||
{
|
||||
"type": "FOOTER",
|
||||
"text": self.footer,
|
||||
}
|
||||
)
|
||||
# TODO: add more components(buttons, location, etc)
|
||||
return components
|
||||
|
||||
def button_sync_template(self):
|
||||
self.ensure_one()
|
||||
gateway = self.gateway_id
|
||||
template_url = url_join(
|
||||
BASE_URL,
|
||||
f"{self.template_uid}",
|
||||
)
|
||||
try:
|
||||
response = requests.get(
|
||||
template_url,
|
||||
headers={"Authorization": "Bearer %s" % gateway.token},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
json_data = response.json()
|
||||
vals = self._prepare_values_to_import(gateway, json_data)
|
||||
self.write(vals)
|
||||
except Exception as err:
|
||||
raise UserError(str(err)) from err
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "reload",
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_values_to_import(self, gateway, json_data):
|
||||
vals = {
|
||||
"name": json_data.get("name").replace("_", " ").title(),
|
||||
"template_name": json_data.get("name"),
|
||||
"category": json_data.get("category").lower(),
|
||||
"language": json_data.get("language"),
|
||||
"state": json_data.get("status").lower(),
|
||||
"template_uid": json_data.get("id"),
|
||||
"gateway_id": gateway.id,
|
||||
}
|
||||
is_supported = True
|
||||
for component in json_data.get("components", []):
|
||||
if component["type"] == "HEADER" and component["format"] == "TEXT":
|
||||
vals["header"] = component["text"]
|
||||
elif component["type"] == "BODY":
|
||||
vals["body"] = component["text"]
|
||||
elif component["type"] == "FOOTER":
|
||||
vals["footer"] = component["text"]
|
||||
else:
|
||||
is_supported = False
|
||||
vals["is_supported"] = is_supported
|
||||
return vals
|
|
@ -19,6 +19,7 @@ In order to make it you must follow this steps:
|
|||
|
||||
* Use the Meta App authentication key as `Token` field
|
||||
* Use the Meta App Phone Number ID as `Whatsapp from Phone` field
|
||||
* Use the Meta Account Business ID as `Whatsapp account` field (only if you need sync templates)
|
||||
* Write your own `Webhook key`
|
||||
* Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
|
||||
* Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
* Olga Marco <olga.marco@creublanca.es>
|
||||
* Enric Tobella <etobella@creublanca.es>
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* Carlos Lopez
|
|
@ -0,0 +1,4 @@
|
|||
**WhatsApp templates**
|
||||
|
||||
* Add support for `Variables`
|
||||
* Add support for `Buttons`
|
|
@ -1,2 +1,4 @@
|
|||
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
|
||||
|
|
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mail_whatsapp_template_rule" model="ir.rule">
|
||||
<field name="name">WhatsApp template: multicompany</field>
|
||||
<field name="model_id" ref="model_mail_whatsapp_template" />
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -8,10 +8,11 @@
|
|||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
|
@ -274,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
|||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
|
@ -300,7 +301,7 @@ span.option {
|
|||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
|
@ -381,12 +382,13 @@ of partners in an integrated way.</p>
|
|||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-4">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="toc-entry-9">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-5">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="toc-entry-10">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-11">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -412,6 +414,7 @@ In order to make it you must follow this steps:</p>
|
|||
<ul class="simple">
|
||||
<li>Use the Meta App authentication key as <cite>Token</cite> field</li>
|
||||
<li>Use the Meta App Phone Number ID as <cite>Whatsapp from Phone</cite> field</li>
|
||||
<li>Use the Meta Account Business ID as <cite>Whatsapp account</cite> field (only if you need sync templates)</li>
|
||||
<li>Write your own <cite>Webhook key</cite></li>
|
||||
<li>Use the Application Secret Key on <cite>Whatsapp Security Key</cite>. It will be used in order to validate the data</li>
|
||||
<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>
|
|||
<li>Now you will be able to respond and receive messages to this person.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Known issues / Roadmap</a></h1>
|
||||
<p><strong>WhatsApp templates</strong></p>
|
||||
<ul class="simple">
|
||||
<li>Add support for <cite>Variables</cite></li>
|
||||
<li>Add support for <cite>Buttons</cite></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
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
|
|||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
|
||||
<h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Creu Blanca</li>
|
||||
<li>Dixmit</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">Contributors</a></h2>
|
||||
<ul>
|
||||
<li><p class="first">Olga Marco <<a class="reference external" href="mailto:olga.marco@creublanca.es">olga.marco@creublanca.es</a>></p>
|
||||
</li>
|
||||
<li><p class="first">Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></p>
|
||||
</li>
|
||||
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
|
||||
<blockquote>
|
||||
<ul class="simple">
|
||||
<li>Olga Marco <<a class="reference external" href="mailto:olga.marco@creublanca.es">olga.marco@creublanca.es</a>></li>
|
||||
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li>
|
||||
<li>Carlos Lopez</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">Other credits</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">Other credits</a></h2>
|
||||
<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>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">Maintainers</a></h2>
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import test_mail_gateway_whatsapp
|
||||
from . import test_mail_whatsapp_template
|
||||
|
|
|
@ -6,7 +6,10 @@ import hmac
|
|||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form, RecordCapturer
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
@ -14,7 +17,7 @@ from odoo.addons.mail_gateway.tests.common import MailGatewayTestCase
|
|||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestMailGatewayTelegram(MailGatewayTestCase):
|
||||
class TestMailGatewayWhatsApp(MailGatewayTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
@ -28,6 +31,17 @@ class TestMailGatewayTelegram(MailGatewayTestCase):
|
|||
"webhook_secret": "MY-SECRET",
|
||||
}
|
||||
)
|
||||
cls.ws_template = cls.env["mail.whatsapp.template"].create(
|
||||
{
|
||||
"name": "New template",
|
||||
"category": "marketing",
|
||||
"language": "es",
|
||||
"body": "Demo template",
|
||||
"state": "approved",
|
||||
"is_supported": True,
|
||||
"gateway_id": cls.gateway.id,
|
||||
}
|
||||
)
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{"name": "Partner", "mobile": "+34 600 000 000"}
|
||||
)
|
||||
|
@ -279,6 +293,65 @@ class TestMailGatewayTelegram(MailGatewayTestCase):
|
|||
)
|
||||
self.assertEqual(message.notification_ids.notification_status, "exception")
|
||||
|
||||
def test_send_message_text(self):
|
||||
"""
|
||||
Test that the message is sent correctly
|
||||
- First message need a template
|
||||
- Second message does not need a template
|
||||
"""
|
||||
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,
|
||||
}
|
||||
self.gateway.whatsapp_account_id = "123456"
|
||||
form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx))
|
||||
form_composer.body = "Body test"
|
||||
self.assertTrue(form_composer.is_required_template)
|
||||
self.assertTrue(form_composer._get_modifier("template_id", "required"))
|
||||
form_composer.template_id = self.ws_template
|
||||
composer = form_composer.save()
|
||||
self.assertEqual(composer.body, "Demo template")
|
||||
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()
|
||||
self.assertEqual(len(capture.records), 1)
|
||||
self.assertEqual(capture.records.body, Markup("<p>Demo template</p>"))
|
||||
# second message
|
||||
form_composer = Form(self.env["whatsapp.composer"].with_context(**ctx))
|
||||
form_composer.body = "Body test"
|
||||
self.assertFalse(form_composer.is_required_template)
|
||||
self.assertFalse(form_composer._get_modifier("template_id", "required"))
|
||||
composer = form_composer.save()
|
||||
self.assertEqual(composer.body, "Body test")
|
||||
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()
|
||||
self.assertEqual(len(capture.records), 1)
|
||||
self.assertEqual(capture.records.body, Markup("<p>Body test</p>"))
|
||||
|
||||
def test_compose(self):
|
||||
self.gateway.webhook_key = self.webhook
|
||||
self.gateway.set_webhook()
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
# Copyright 2024 Tecnativa - Carlos López
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
from odoo.addons.mail_gateway.tests.common import MailGatewayTestCase
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestMailWhatsAppTemplate(MailGatewayTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.gateway = cls.env["mail.gateway"].create(
|
||||
{
|
||||
"name": "gateway",
|
||||
"gateway_type": "whatsapp",
|
||||
"token": "token",
|
||||
"whatsapp_security_key": "key",
|
||||
"webhook_secret": "MY-SECRET",
|
||||
}
|
||||
)
|
||||
cls.new_template_response_data = {
|
||||
"id": "018273645",
|
||||
"status": "APPROVED",
|
||||
}
|
||||
cls.new_template_full_response_data = {
|
||||
"name": "new_template",
|
||||
"parameter_format": "POSITIONAL",
|
||||
"components": [
|
||||
{"type": "HEADER", "format": "TEXT", "text": "Header 1"},
|
||||
{"type": "BODY", "text": "Body 1"},
|
||||
{"type": "FOOTER", "text": "Footer changed"},
|
||||
],
|
||||
"language": "es",
|
||||
"status": "APPROVED",
|
||||
"category": "MARKETING",
|
||||
"id": "018273645",
|
||||
}
|
||||
cls.template_1_data = {
|
||||
"name": "test_odoo_1",
|
||||
"parameter_format": "POSITIONAL",
|
||||
"components": [
|
||||
{"type": "HEADER", "format": "TEXT", "text": "Header 1"},
|
||||
{"type": "BODY", "text": "Body 1"},
|
||||
{"type": "FOOTER", "text": "Footer 1"},
|
||||
],
|
||||
"language": "es",
|
||||
"status": "APPROVED",
|
||||
"category": "MARKETING",
|
||||
"id": "1234567890",
|
||||
}
|
||||
cls.template_2_data = {
|
||||
"name": "test_with_buttons",
|
||||
"parameter_format": "POSITIONAL",
|
||||
"components": [
|
||||
{"type": "HEADER", "format": "TEXT", "text": "Header 2"},
|
||||
{"type": "BODY", "text": "Body 2"},
|
||||
{
|
||||
"type": "BUTTONS",
|
||||
"buttons": [{"type": "QUICK_REPLY", "text": "Button 1"}],
|
||||
},
|
||||
],
|
||||
"language": "es",
|
||||
"status": "APPROVED",
|
||||
"category": "MARKETING",
|
||||
"sub_category": "CUSTOM",
|
||||
"id": "0987654321",
|
||||
}
|
||||
cls.templates_download = {
|
||||
"data": [cls.template_1_data, cls.template_2_data],
|
||||
}
|
||||
|
||||
def _make_meta_requests(self, url, json_data, status_code=200):
|
||||
"""
|
||||
Simulate a fake request to the Meta API:
|
||||
:param json_data: Dictionary with the json data to return
|
||||
:param status_code: Status code expected
|
||||
:returns requests.Response object
|
||||
"""
|
||||
response = requests.Response()
|
||||
response.status_code = status_code
|
||||
response._content = json.dumps(json_data).encode()
|
||||
response.url = url
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
def test_download_templates(self):
|
||||
def _patch_request_post(url, *args, **kwargs):
|
||||
if "message_templates" in url:
|
||||
return self._make_meta_requests(url, self.templates_download)
|
||||
return original_get(url, *args, **kwargs)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
UserError, "WhatsApp Account is required to import templates"
|
||||
):
|
||||
self.gateway.button_import_whatsapp_template()
|
||||
self.gateway.whatsapp_account_id = "123456"
|
||||
original_get = requests.get
|
||||
with patch.object(requests, "get", _patch_request_post):
|
||||
self.gateway.button_import_whatsapp_template()
|
||||
self.assertEqual(self.gateway.whatsapp_template_count, 2)
|
||||
template_1 = self.gateway.whatsapp_template_ids.filtered(
|
||||
lambda t: t.template_uid == "1234567890"
|
||||
)
|
||||
self.assertTrue(template_1.is_supported)
|
||||
self.assertEqual(template_1.template_name, "test_odoo_1")
|
||||
self.assertEqual(template_1.category, "marketing")
|
||||
self.assertEqual(template_1.language, "es")
|
||||
self.assertEqual(template_1.state, "approved")
|
||||
self.assertEqual(template_1.header, "Header 1")
|
||||
self.assertEqual(template_1.body, "Body 1")
|
||||
self.assertEqual(template_1.footer, "Footer 1")
|
||||
template_2 = self.gateway.whatsapp_template_ids.filtered(
|
||||
lambda t: t.template_uid == "0987654321"
|
||||
)
|
||||
self.assertFalse(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")
|
||||
self.assertEqual(template_2.state, "approved")
|
||||
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):
|
||||
if "message_templates" in url:
|
||||
return self._make_meta_requests(url, self.new_template_response_data)
|
||||
return original_post(url, *args, **kwargs)
|
||||
|
||||
def _patch_request_get(url, *args, **kwargs):
|
||||
if "018273645" in url:
|
||||
return self._make_meta_requests(
|
||||
url, self.new_template_full_response_data
|
||||
)
|
||||
return original_get(url, *args, **kwargs)
|
||||
|
||||
original_post = requests.post
|
||||
original_get = requests.get
|
||||
self.gateway.whatsapp_account_id = "123456"
|
||||
new_template = self.env["mail.whatsapp.template"].create(
|
||||
{
|
||||
"name": "New template",
|
||||
"category": "marketing",
|
||||
"language": "es",
|
||||
"header": "Header 1",
|
||||
"body": "Body 1",
|
||||
"gateway_id": self.gateway.id,
|
||||
}
|
||||
)
|
||||
self.assertEqual(new_template.template_name, "new_template")
|
||||
with patch.object(requests, "post", _patch_request_post):
|
||||
new_template.button_export_template()
|
||||
self.assertTrue(new_template.template_uid)
|
||||
self.assertTrue(new_template.is_supported)
|
||||
self.assertFalse(new_template.footer)
|
||||
self.assertEqual(new_template.state, "approved")
|
||||
# sync templates, footer should be updated
|
||||
with patch.object(requests, "get", _patch_request_get):
|
||||
new_template.button_sync_template()
|
||||
self.assertEqual(new_template.footer, "Footer changed")
|
|
@ -0,0 +1 @@
|
|||
from . import const
|
|
@ -0,0 +1,75 @@
|
|||
# https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/supported-languages # noqa: B950
|
||||
# res.lang not matching with supported languages(iso codes)
|
||||
supported_languages = [
|
||||
("af", "Afrikaans"),
|
||||
("sq", "Albanian"),
|
||||
("ar", "Arabic"),
|
||||
("az", "Azerbaijani"),
|
||||
("bn", "Bengali"),
|
||||
("bg", "Bulgarian"),
|
||||
("ca", "Catalan"),
|
||||
("zh_CN", "Chinese (CHN)"),
|
||||
("zh_HK", "Chinese (HKG)"),
|
||||
("zh_TW", "Chinese (TAI)"),
|
||||
("hr", "Croatian"),
|
||||
("cs", "Czech"),
|
||||
("da", "Danish"),
|
||||
("nl", "Dutch"),
|
||||
("en", "English"),
|
||||
("en_GB", "English (UK)"),
|
||||
("en_US", "English (US)"),
|
||||
("et", "Estonian"),
|
||||
("fil", "Filipino"),
|
||||
("fi", "Finnish"),
|
||||
("fr", "French"),
|
||||
("ka", "Georgian"),
|
||||
("de", "German"),
|
||||
("el", "Greek"),
|
||||
("gu", "Gujarati"),
|
||||
("ha", "Hausa"),
|
||||
("he", "Hebrew"),
|
||||
("hi", "Hindi"),
|
||||
("hu", "Hungarian"),
|
||||
("id", "Indonesian"),
|
||||
("ga", "Irish"),
|
||||
("it", "Italian"),
|
||||
("ja", "Japanese"),
|
||||
("kn", "Kannada"),
|
||||
("kk", "Kazakh"),
|
||||
("rw_RW", "Kinyarwanda"),
|
||||
("ko", "Korean"),
|
||||
("ky_KG", "Kyrgyz (Kyrgyzstan)"),
|
||||
("lo", "Lao"),
|
||||
("lv", "Latvian"),
|
||||
("lt", "Lithuanian"),
|
||||
("mk", "Macedonian"),
|
||||
("ms", "Malay"),
|
||||
("ml", "Malayalam"),
|
||||
("mr", "Marathi"),
|
||||
("nb", "Norwegian"),
|
||||
("fa", "Persian"),
|
||||
("pl", "Polish"),
|
||||
("pt_BR", "Portuguese (BR)"),
|
||||
("pt_PT", "Portuguese (POR)"),
|
||||
("pa", "Punjabi"),
|
||||
("ro", "Romanian"),
|
||||
("ru", "Russian"),
|
||||
("sr", "Serbian"),
|
||||
("sk", "Slovak"),
|
||||
("sl", "Slovenian"),
|
||||
("es", "Spanish"),
|
||||
("es_AR", "Spanish (ARG)"),
|
||||
("es_ES", "Spanish (SPA)"),
|
||||
("es_MX", "Spanish (MEX)"),
|
||||
("sw", "Swahili"),
|
||||
("sv", "Swedish"),
|
||||
("ta", "Tamil"),
|
||||
("te", "Telugu"),
|
||||
("th", "Thai"),
|
||||
("tr", "Turkish"),
|
||||
("uk", "Ukrainian"),
|
||||
("ur", "Urdu"),
|
||||
("uz", "Uzbek"),
|
||||
("vi", "Vietnamese"),
|
||||
("zu", "Zulu"),
|
||||
]
|
|
@ -7,7 +7,35 @@
|
|||
<field name="model">mail.gateway</field>
|
||||
<field name="inherit_id" ref="mail_gateway.mail_gateway_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button
|
||||
type="object"
|
||||
name="button_import_whatsapp_template"
|
||||
string="Download Templates"
|
||||
attrs="{'invisible': [('gateway_type', '!=', 'whatsapp')]}"
|
||||
icon="fa-download"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button
|
||||
type="action"
|
||||
name="%(mail_gateway_whatsapp.action_mail_whatsapp_template_gateway)d"
|
||||
attrs="{'invisible': [('gateway_type', '!=', 'whatsapp')]}"
|
||||
class="oe_stat_button"
|
||||
icon="fa-whatsapp"
|
||||
>
|
||||
<field
|
||||
name="whatsapp_template_count"
|
||||
string="Templates"
|
||||
widget="statinfo"
|
||||
/>
|
||||
</button>
|
||||
</xpath>
|
||||
<field name="webhook_user_id" position="after">
|
||||
<field
|
||||
name="whatsapp_account_id"
|
||||
attrs="{'invisible': [('gateway_type', '!=', 'whatsapp')]}"
|
||||
/>
|
||||
<field
|
||||
name="whatsapp_security_key"
|
||||
attrs="{'invisible': [('gateway_type', '!=', 'whatsapp')]}"
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_mail_whatsapp_template_tree" model="ir.ui.view">
|
||||
<field name="name">view.mail.whatsapp.template.tree</field>
|
||||
<field name="model">mail.whatsapp.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-danger="not is_supported and template_uid">
|
||||
<field name="name" />
|
||||
<field name="template_name" />
|
||||
<field name="template_uid" optional="show" />
|
||||
<field name="category" />
|
||||
<field name="language" />
|
||||
<field name="gateway_id" />
|
||||
<field
|
||||
name="company_id"
|
||||
options="{'no_create': True}"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
decoration-success="state == 'approved'"
|
||||
decoration-danger="state == 'rejected'"
|
||||
widget="badge"
|
||||
/>
|
||||
<field name="is_supported" optional="show" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_mail_whatsapp_template_form" model="ir.ui.view">
|
||||
<field name="name">view.mail.whatsapp.template.form</field>
|
||||
<field name="model">mail.whatsapp.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button
|
||||
string="Export Template"
|
||||
name="button_export_template"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
attrs="{'invisible': [('template_uid', '!=', False)]}"
|
||||
/>
|
||||
<button
|
||||
string="Sync Template"
|
||||
name="button_sync_template"
|
||||
type="object"
|
||||
attrs="{'invisible': [('template_uid', '=', False)]}"
|
||||
/>
|
||||
<button
|
||||
string="Back to draft"
|
||||
name="button_back2draft"
|
||||
type="object"
|
||||
attrs="{'invisible': [('state', '=', 'draft')]}"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,pending,approved"
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
class="alert alert-danger"
|
||||
role="alert"
|
||||
attrs="{'invisible': ['|',('is_supported', '=', True), ('template_uid', '=', False)]}"
|
||||
>
|
||||
This template is not supported because has <strong
|
||||
>variables</strong> or <strong>buttons</strong>.
|
||||
</div>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" />
|
||||
<h1>
|
||||
<field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="gateway_id"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="category"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<field
|
||||
name="language"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="header"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<field
|
||||
name="footer"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
<field name="template_name" />
|
||||
<field name="template_uid" />
|
||||
<field
|
||||
name="company_id"
|
||||
options="{'no_create': True}"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="is_supported" invisible="1" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="body" string="Body">
|
||||
<field
|
||||
name="body"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_mail_whatsapp_template_search" model="ir.ui.view">
|
||||
<field name="name">view.mail.whatsapp.template.search</field>
|
||||
<field name="model">mail.whatsapp.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" />
|
||||
<field name="gateway_id" />
|
||||
<filter
|
||||
name="group_by_state"
|
||||
string="State"
|
||||
context="{'group_by': 'state'}"
|
||||
/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mail_whatsapp_template_gateway" model="ir.actions.act_window">
|
||||
<field name="name">WhatsApp Templates</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mail.whatsapp.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('gateway_id', '=', active_id)]</field>
|
||||
<field name="context">{'default_gateway_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -1 +1,2 @@
|
|||
from . import mail_compose_gateway_message
|
||||
from . import whatsapp_composer
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2024 Tecnativa - Carlos López
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import markupsafe
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailComposeGatewayMessage(models.TransientModel):
|
||||
_inherit = "mail.compose.gateway.message"
|
||||
|
||||
whatsapp_template_id = fields.Many2one(
|
||||
"mail.whatsapp.template",
|
||||
domain="""[
|
||||
('state', '=', 'approved'),
|
||||
('is_supported', '=', True)
|
||||
]""",
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
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)
|
||||
return super(MailComposeGatewayMessage, self)._action_send_mail(
|
||||
auto_commit=auto_commit
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2024 Tecnativa - Carlos López
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="mail_compose_gateway_message_form_view">
|
||||
<field name="model">mail.compose.gateway.message</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="mail_gateway.mail_compose_gateway_message_form_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='wizard_channel_ids']" position="after">
|
||||
<field
|
||||
name="whatsapp_template_id"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='template_id']" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'invisible': [('whatsapp_template_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='attachment_ids']" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'invisible': [('whatsapp_template_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright 2022 CreuBlanca
|
||||
# Copyright 2024 Tecnativa - Carlos López
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
@ -17,7 +19,57 @@ class WhatsappComposer(models.TransientModel):
|
|||
gateway_id = fields.Many2one(
|
||||
"mail.gateway", domain=[("gateway_type", "=", "whatsapp")], required=True
|
||||
)
|
||||
template_id = fields.Many2one(
|
||||
"mail.whatsapp.template",
|
||||
domain="""[
|
||||
('gateway_id', '=', gateway_id),
|
||||
('state', '=', 'approved'),
|
||||
('is_supported', '=', True)
|
||||
]""",
|
||||
)
|
||||
body = fields.Text("Message")
|
||||
is_required_template = fields.Boolean(compute="_compute_is_required_template")
|
||||
|
||||
@api.depends("res_model", "res_id", "number_field_name", "gateway_id")
|
||||
def _compute_is_required_template(self):
|
||||
MailMessage = self.env["mail.message"]
|
||||
for wizard in self:
|
||||
if (
|
||||
not wizard.res_model
|
||||
or not wizard.gateway_id
|
||||
or not wizard.number_field_name
|
||||
):
|
||||
wizard.is_required_template = False
|
||||
continue
|
||||
record = self.env[wizard.res_model].browse(wizard.res_id)
|
||||
is_required_template = True
|
||||
channel = record._whatsapp_get_channel(
|
||||
wizard.number_field_name, wizard.gateway_id
|
||||
)
|
||||
if channel:
|
||||
last_message = MailMessage.search(
|
||||
[
|
||||
("gateway_type", "=", "whatsapp"),
|
||||
("model", "=", channel._name),
|
||||
("res_id", "=", channel.id),
|
||||
],
|
||||
order="date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_message:
|
||||
delta = (datetime.now() - last_message.date).total_seconds() / 3600
|
||||
if delta < 24.0:
|
||||
is_required_template = False
|
||||
wizard.is_required_template = is_required_template
|
||||
|
||||
@api.onchange("gateway_id")
|
||||
def onchange_gateway_id(self):
|
||||
self.template_id = False
|
||||
|
||||
@api.onchange("template_id")
|
||||
def onchange_template_id(self):
|
||||
if self.template_id:
|
||||
self.body = self.template_id.body
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
|
@ -33,7 +85,7 @@ class WhatsappComposer(models.TransientModel):
|
|||
if not record:
|
||||
return
|
||||
channel = record._whatsapp_get_channel(self.number_field_name, self.gateway_id)
|
||||
channel.message_post(
|
||||
channel.with_context(whatsapp_template_id=self.template_id.id).message_post(
|
||||
body=self.body, subtype_xmlid="mail.mt_comment", message_type="comment"
|
||||
)
|
||||
|
||||
|
|
|
@ -13,11 +13,21 @@
|
|||
name="gateway_id"
|
||||
attrs="{'invisible': [('find_gateway', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="template_id"
|
||||
attrs="{'required': [('is_required_template', '=', True)]}"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
/>
|
||||
<field name="find_gateway" invisible="1" />
|
||||
<field name="res_model" invisible="1" />
|
||||
<field name="res_id" invisible="1" />
|
||||
<field name="number_field_name" invisible="1" />
|
||||
<field name="body" />
|
||||
<field name="is_required_template" invisible="1" />
|
||||
<field
|
||||
name="body"
|
||||
attrs="{'readonly': [('template_id', '!=', False)]}"
|
||||
force_save="1"
|
||||
/>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
|
|
Loading…
Reference in New Issue