Skip to content

Commit

Permalink
Merge PR #1497 into 16.0
Browse files Browse the repository at this point in the history
Signed-off-by pedrobaeza
  • Loading branch information
OCA-git-bot committed Jan 3, 2025
2 parents a4eeca5 + 455fa12 commit 7cf36fa
Show file tree
Hide file tree
Showing 27 changed files with 992 additions and 31 deletions.
8 changes: 3 additions & 5 deletions mail_gateway/models/mail_gateway.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command, api, fields, models, tools
from odoo import api, fields, models, tools


class MailGateway(models.Model):
Expand All @@ -25,12 +25,10 @@ class MailGateway(models.Model):
)
webhook_user_id = fields.Many2one(
"res.users",
default=lambda self: self.env.user.id,
default=lambda self: self.env.ref("base.user_root"),
help="User that will create the messages",
)
member_ids = fields.Many2many(
"res.users", default=lambda self: [Command.link(self.env.user.id)]
)
member_ids = fields.Many2many("res.users")
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company.id
)
Expand Down
7 changes: 6 additions & 1 deletion mail_gateway_telegram/tests/test_mail_gateway_telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ def setUpClass(cls):
super().setUpClass()
cls.webhook = "demo_hook"
cls.gateway = cls.env["mail.gateway"].create(
{"name": "gateway", "gateway_type": "telegram", "token": "token"}
{
"name": "gateway",
"gateway_type": "telegram",
"token": "token",
"member_ids": [(4, cls.env.user.id)],
}
)
cls.password = "my_new_password"
cls.gateway_token = "12341234"
Expand Down
12 changes: 12 additions & 0 deletions mail_gateway_whatsapp/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
===========

Expand All @@ -103,6 +112,9 @@ Contributors

* Olga Marco <[email protected]>
* Enric Tobella <[email protected]>
* `Tecnativa <https://www.tecnativa.com>`_:

* Carlos Lopez

Other credits
~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions mail_gateway_whatsapp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import models
from . import tools

# from . import services
from . import wizards
3 changes: 3 additions & 0 deletions mail_gateway_whatsapp/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions mail_gateway_whatsapp/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from . import mail_gateway_whatsapp
from . import mail_channel
from . import res_partner
from . import mail_whatsapp_template
62 changes: 61 additions & 1 deletion mail_gateway_whatsapp/models/mail_gateway.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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"},
},
}
27 changes: 24 additions & 3 deletions mail_gateway_whatsapp/models/mail_gateway_whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,35 @@ def _send(
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":
Expand Down
193 changes: 193 additions & 0 deletions mail_gateway_whatsapp/models/mail_whatsapp_template.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 7cf36fa

Please sign in to comment.