[MIG] mail_gateway: Migration to 16.0

pull/1305/head
Enric Tobella 2024-01-29 13:13:44 +01:00
parent e29d5198c9
commit 1fcae5c8b9
87 changed files with 2384 additions and 1532 deletions

View File

@ -1,13 +1,13 @@
===========
Mail Broker
===========
============
Mail Gateway
============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:1708757421780a7323d97082c1710d228ae9283bf9c9fbc1f5dc35ca1a8381d0
!! source digest: sha256:801096a9a4e9f69df86b66ba9592250e32272c7f301eea5fee5b2824aa5fe175
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@ -17,10 +17,10 @@ Mail Broker
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github
:target: https://github.com/OCA/social/tree/16.0/mail_broker
:target: https://github.com/OCA/social/tree/16.0/mail_gateway
:alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_broker
:target: https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_gateway
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=16.0
@ -28,26 +28,37 @@ Mail Broker
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows to respond chats as a bot.
This module will allow you to integrate an external chat system in your Odoo system.
It requires extra modules with the specific configuration of each chat system, like `mail_gateway_telegram` or `mail_gateway_whatsapp`.
This way, a group of users can respond customers or any other set
of partners in an integrated way.
It is not intended to be integrated on default chatter as users don't need
to review again when one has responded.
of partners within Odoo, but the messages will be sent through the external chat system.
**Table of contents**
.. contents::
:local:
Usage
=====
When external messages are received, they will be directly sent to the discuss menu.
Answering to these messages will send the answer to the external contact.
We can assign this messages to any record using the message actions.
Also, we can assign the sender to a partner using the followers menu and selecting the partner.
On a standard record associated to a partner with external chat, we can send messages to the external contact directly selecting the methods of the partner.
To use this, we just need to use the
It is recomended to enable chatter notification to all users that will receive messages from gateways.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
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
`feedback <https://github.com/OCA/social/issues/new?body=module:%20mail_broker%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/social/issues/new?body=module:%20mail_gateway%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -58,12 +69,18 @@ Authors
~~~~~~~
* Creu Blanca
* Dixmit
Contributors
~~~~~~~~~~~~
* Enric Tobella <etobella@creublanca.es>
* Olga Marco <olga.marco@creublanca.es>
* Enric Tobella
* Olga Marco
Other credits
~~~~~~~~~~~~~
This work has been funded by AEOdoo (Asociación Española de Odoo - https://www.aeodoo.org)
Maintainers
~~~~~~~~~~~
@ -78,6 +95,6 @@ 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.
This module is part of the `OCA/social <https://github.com/OCA/social/tree/16.0/mail_broker>`_ project on GitHub.
This module is part of the `OCA/social <https://github.com/OCA/social/tree/16.0/mail_gateway>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -1,4 +1,6 @@
from . import controllers
from . import models
from . import services
# from . import services
from .hooks import pre_init_hook
from . import wizards

View File

@ -1,22 +1,38 @@
# Copyright 2020 Creu Blanca
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Mail Broker",
"name": "Mail Gateway",
"summary": """
Set a broker""",
"version": "14.0.1.0.0",
Set a gateway""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Creu Blanca,Odoo Community Association (OCA)",
"author": "Creu Blanca,Dixmit,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
"qweb": ["static/src/xml/broker.xml"],
"depends": ["mail", "base_rest"],
"depends": ["mail"],
"pre_init_hook": "pre_init_hook",
"data": [
"wizards/mail_message_gateway_link.xml",
"wizards/mail_message_gateway_send.xml",
"wizards/mail_guest_manage.xml",
"security/security.xml",
"security/ir.model.access.csv",
"views/mail_broker.xml",
"templates/assets.xml",
"views/mail_broker_channel.xml",
"views/mail_gateway.xml",
"views/res_partner_gateway_channel.xml",
],
"assets": {
"mail.assets_messaging": [
"mail_gateway/static/src/models/**/*.js",
],
"web.assets_backend": [
"mail_gateway/static/src/components/**/*.xml",
"mail_gateway/static/src/components/**/*.js",
"mail_gateway/static/src/components/**/*.scss",
],
"mail.assets_discuss_public": [
"mail_gateway/static/src/components/**/*.xml",
"mail_gateway/static/src/components/**/*.js",
"mail_gateway/static/src/components/**/*.scss",
],
},
}

View File

@ -1,2 +1,2 @@
from . import main
from . import mail_broker
from . import gateway
from . import discuss

View File

@ -0,0 +1,11 @@
# Copyright 2024 Dixmit
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.mail.controllers.discuss import DiscussController
class GatewayDiscussController(DiscussController):
def _get_allowed_message_post_params(self):
result = super()._get_allowed_message_post_params()
result.add("gateway_notifications")
return result

View File

@ -0,0 +1,81 @@
# Copyright 2024 Dixmit
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import json
import logging
from odoo.http import Controller, request, route
_logger = logging.getLogger(__name__)
class GatewayController(Controller):
@route(
"/gateway/<string:usage>/<string:token>/update",
type="http",
auth="public",
methods=["GET", "POST"],
csrf=False,
)
def post_update(self, usage, token, *args, **kwargs):
if request.httprequest.method == "GET":
bot_data = request.env["mail.gateway"]._get_gateway(
token, gateway_type=usage, state="pending"
)
if not bot_data:
return request.make_response(
json.dumps({}),
[
("Content-Type", "application/json"),
],
)
return (
request.env["mail.gateway.%s" % usage]
.with_user(bot_data["webhook_user_id"])
._receive_get_update(bot_data, request, **kwargs)
)
bot_data = request.env["mail.gateway"]._get_gateway(
token, gateway_type=usage, state="integrated"
)
if not bot_data:
_logger.warning(
"Gateway was not found for token %s with usage %s", token, usage
)
return request.make_response(
json.dumps({}),
[
("Content-Type", "application/json"),
],
)
jsonrequest = json.loads(
request.httprequest.get_data().decode(request.httprequest.charset)
)
dispatcher = (
request.env["mail.gateway.%s" % usage]
.with_user(bot_data["webhook_user_id"])
.with_context(no_gateway_notification=True)
)
if not dispatcher._verify_update(bot_data, jsonrequest):
_logger.warning(
"Message could not be verified for token %s with usage %s", token, usage
)
return request.make_response(
json.dumps({}),
[
("Content-Type", "application/json"),
],
)
_logger.debug(
"Received message for token %s with usage %s: %s",
token,
usage,
json.dumps(jsonrequest),
)
gateway = dispatcher.env["mail.gateway"].browse(bot_data["id"])
dispatcher._receive_update(gateway, jsonrequest)
return request.make_response(
json.dumps({}),
[
("Content-Type", "application/json"),
],
)

View File

@ -1,10 +0,0 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest.controllers import main
class MailBrokerController(main.RestController):
_root_path = "/broker/"
_collection_name = "mail.broker"
_default_auth = "none"

View File

@ -1,26 +0,0 @@
from odoo import http
from odoo.http import request
from odoo.addons.mail.controllers.bus import MailChatController
from odoo.addons.mail.controllers.main import MailController
class NewMailController(MailController):
@http.route("/mail/init_messaging", type="json", auth="user")
def mail_init_messaging(self):
result = super().mail_init_messaging()
result["broker_slots"] = request.env["mail.broker"].broker_fetch_slot()
return result
class NewMailChatController(MailChatController):
def _poll(self, dbname, channels, last, options):
if request.session.uid:
if request.env.user.has_group("mail_broker.broker_user"):
channels = list(channels)
for channel in request.env["mail.channel"].search(
[("public", "=", "broker")]
):
channels.append((request.db, "mail.channel", channel.id))
result = super()._poll(dbname, channels, last, options)
return result

View File

@ -8,5 +8,5 @@ def pre_init_hook(cr):
"""
cr.execute(
"""ALTER TABLE mail_message
ADD COLUMN broker_channel_id int"""
ADD COLUMN gateway_channel_id int"""
)

View File

@ -1,4 +1,10 @@
from . import mail_message
from . import mail_message_broker
from . import mail_broker_channel
from . import mail_broker
from . import mail_notification
from . import mail_channel
from . import mail_gateway
from . import ir_websocket
from . import res_partner
from . import mail_guest
from . import mail_gateway_abstract
from . import res_users
from . import mail_thread

View File

@ -0,0 +1,21 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
from odoo.http import request
from odoo.addons.bus.websocket import wsrequest
class IrWebsocket(models.AbstractModel):
_inherit = "ir.websocket"
def _build_bus_channel_list(self, channels):
req = request or wsrequest
result = super()._build_bus_channel_list(channels)
if req.session.uid:
if req.env.user.has_group("mail_gateway.gateway_user"):
for channel in req.env["mail.channel"].search(
[("channel_type", "=", "gateway")]
):
result.append(channel)
return result

View File

@ -1,162 +0,0 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, tools
class MailBroker(models.Model):
_name = "mail.broker"
_inherit = ["collection.base"]
_description = "Mail Broker"
name = fields.Char(required=True)
token = fields.Char(required=True)
broker_type = fields.Selection([], required=True)
show_on_app = fields.Boolean(default=True)
webhook_key = fields.Char()
webhook_secret = fields.Char()
integrated_webhook_state = fields.Selection(
[("pending", "Pending"), ("integrated", "Integrated")], readonly=True
)
can_set_webhook = fields.Boolean(compute="_compute_webhook_checks")
webhook_url = fields.Char(compute="_compute_webhook_url")
has_new_channel_security = fields.Boolean(
help="When checked, channels are not created automatically"
)
webhook_user_id = fields.Many2one(
"res.users", default=lambda self: self.env.user.id
)
_sql_constraints = [
("mail_broker_token", "unique(token)", "Token must be unique"),
(
"mail_broker_webhook_key",
"unique(webhook_key)",
"Webhook Key must be unique",
),
]
@api.depends("webhook_key")
def _compute_webhook_url(self):
for record in self:
record.webhook_url = record._get_webhook_url()
def _get_channel_id(self, chat_token):
return (
self.env["mail.channel"]
.search(
[("token", "=", str(chat_token)), ("broker_id", "=", self.id)],
limit=1,
)
.id
)
def _get_webhook_url(self):
return "%s/broker/%s/%s/update" % (
self.webhook_url
or self.env["ir.config_parameter"].get_param("web.base.url"),
self.broker_type,
self.webhook_key,
)
def _can_set_webhook(self):
return self.webhook_key and self.webhook_user_id
@api.depends("broker_type")
def _compute_webhook_checks(self):
for record in self:
record.can_set_webhook = record._can_set_webhook()
def set_webhook(self):
self.ensure_one()
if self.can_set_webhook:
with self.work_on(self._name) as work:
work.component(usage=self.broker_type)._set_webhook()
def remove_webhook(self):
self.ensure_one()
with self.work_on(self._name) as work:
work.component(usage=self.broker_type)._remove_webhook()
def update_webhook(self):
self.ensure_one()
self.remove_webhook()
self.set_webhook()
@api.model
def broker_fetch_slot(self):
result = []
for record in self.search([("show_on_app", "=", True)]):
result.append(
{
"id": record.id,
"name": record.name,
"channel_name": "broker_%s" % record.id,
"threads": [
thread._get_thread_data()
for thread in self.env["mail.channel"].search(
[("show_on_app", "=", True), ("broker_id", "=", record.id)]
)
],
}
)
return result
def channel_search(self, name):
self.ensure_one()
domain = [("broker_id", "=", self.id)]
if name:
domain += [("name", "ilike", "%" + name + "%")]
return self.env["mail.channel"].search(domain).read(["name"])
def write(self, vals):
res = super(MailBroker, self).write(vals)
if (
"webhook_key" in vals
or "integrated_webhook_state" in vals
or "webhook_secret" in vals
or "webhook_user_id" in vals
):
self.clear_caches()
return res
@api.model_create_single
def create(self, vals):
res = super(MailBroker, self).create(vals)
if (
"webhook_key" in vals
or "integrated_webhook_state" in vals
or "webhook_secret" in vals
or "webhook_user_id" in vals
):
self.clear_caches()
return res
@api.model
@tools.ormcache()
def _get_broker_map(self, state="integrated", broker_type=False):
result = {}
for record in self.search(
[
("integrated_webhook_state", "=", state),
("broker_type", "=", broker_type),
]
):
result[record.webhook_key] = record._get_broker_data()
return result
def _get_broker_data(self):
return {
"id": self.id,
"webhook_secret": self.webhook_secret,
"webhook_user_id": self.webhook_user_id.id,
}
@api.model
def _get_broker(self, key, state="integrated", broker_type=False, **kwargs):
# We are using cache in order to avoid an exploit
if not key:
return False
return self._get_broker_map(state=state, broker_type=broker_type).get(
key, False
)

View File

@ -1,204 +0,0 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from xmlrpc.client import DateTime
from odoo import api, fields, models
class MailChannel(models.Model):
_inherit = "mail.channel"
token = fields.Char()
broker_id = fields.Many2one("mail.broker")
broker_message_ids = fields.One2many(
"mail.message.broker",
inverse_name="channel_id",
)
last_message_date = fields.Datetime(
compute="_compute_message_data",
store=True,
)
public = fields.Selection(
selection_add=[("broker", "Broker")], ondelete={"broker": "set default"}
)
channel_type = fields.Selection(
selection_add=[("broker", "Broker")], ondelete={"broker": "set default"}
)
broker_unread = fields.Integer(
compute="_compute_message_data",
store=True,
)
broker_token = fields.Char(related="broker_id.token", store=True, required=False)
show_on_app = fields.Boolean()
broker_partner_id = fields.Many2one("res.partner")
def message_fetch(self, domain=False, limit=30):
self.ensure_one()
if not domain:
domain = []
return (
self.env["mail.message"]
.search([("broker_channel_id", "=", self.id)] + domain, limit=limit)
.message_format()
)
@api.depends(
"message_ids",
"message_ids.date",
"message_ids.broker_unread",
)
def _compute_message_data(self):
for r in self:
r.last_message_date = (
self.env["mail.message"]
.search(
[("broker_channel_id", "=", r.id)],
limit=1,
order="date DESC",
)
.date
)
r.broker_unread = self.env["mail.message"].search_count(
[("broker_channel_id", "=", r.id), ("broker_unread", "=", True)]
)
def _get_thread_data(self):
return {
"id": "broker_thread_%s" % self.id,
"res_id": self.id,
"name": self.name,
"last_message_date": self.last_message_date,
"channel_type": "broker_thread",
"unread": self.broker_unread,
"broker_id": self.broker_id.id,
}
def _broker_message_post_vals(
self,
body,
subtype_id=False,
author_id=False,
date=False,
message_id=False,
**kwargs
):
if not subtype_id:
subtype = kwargs.get("subtype") or "mt_note"
if "." not in subtype:
subtype = "mail.%s" % subtype
subtype_id = self.env["ir.model.data"].xmlid_to_res_id(subtype)
vals = {
"channel_id": self.id,
"channel_ids": [(4, self.id)],
"body": body,
"subtype_id": subtype_id,
"model": self._name,
"res_id": self.id,
"broker_type": self.broker_id.broker_type,
}
if author_id:
vals["author_id"] = author_id
if date:
if isinstance(date, DateTime):
date = datetime.strptime(str(date), "%Y%m%dT%H:%M:%S")
vals["date"] = date
if message_id:
vals["message_id"] = message_id
vals["broker_unread"] = kwargs.get("broker_unread", False)
vals["attachment_ids"] = []
for attachment_id in kwargs.get("attachment_ids", []):
vals["attachment_ids"].append((4, attachment_id))
for name, content, mimetype in kwargs.get("attachments", []):
vals["attachment_ids"].append(
(
0,
0,
{
"name": name,
"datas": content.encode("utf-8"),
"type": "binary",
"description": name,
"mimetype": mimetype,
},
)
)
return vals
@api.returns("mail.message.broker", lambda value: value.id)
def message_post_broker(self, body=False, broker_type=False, **kwargs):
self.ensure_one()
if (
not body
and not kwargs.get("attachments")
and not kwargs.get("attachment_ids")
):
return False
vals = self._broker_message_post_vals(
body, broker_unread=True, author_id=self.broker_partner_id.id, **kwargs
)
vals["state"] = "received"
vals["broker_type"] = broker_type
return self.env["mail.message.broker"].create(vals)
@api.returns("mail.message", lambda value: value.id)
def message_post(self, *args, **kwargs):
message = super().message_post(*args, **kwargs)
if self.broker_id:
self.env["mail.message.broker"].create(
{
"mail_message_id": message.id,
"channel_id": self.id,
}
).send()
return message
@api.model
def channel_fetch_slot(self):
result = super().channel_fetch_slot()
broker_channels = self.env["mail.channel"].search([("public", "=", "broker")])
result["channel_channel"] += broker_channels.channel_info()
return result
def channel_info(self, *args, **kwargs):
result = super().channel_info(*args, **kwargs)
for channel, channel_info in zip(self, result):
channel_info["broker_id"] = (
channel.broker_id and channel.broker_id.id or False
)
channel_info["broker_unread_counter"] = channel.broker_unread
return result
@api.model_create_multi
def create(self, vals_list):
channels = super().create(vals_list)
notifications = []
for channel in channels:
if channel.show_on_app and channel.broker_id.show_on_app:
notifications.append(
(
(self._cr.dbname, "mail.broker", channel.broker_id.id),
{"thread": channel._get_thread_data()},
)
)
if notifications:
self.env["bus.bus"].sendmany(notifications)
return channels
@api.returns("mail.message.broker", lambda value: value.id)
def broker_message_post(self, body=False, **kwargs):
self.ensure_one()
if not body and not kwargs.get("attachment_ids"):
return
message = (
self.with_context(do_not_notify=True)
.env["mail.message.broker"]
.create(self._broker_message_post_vals(body, **kwargs))
)
message.send()
self.env["bus.bus"].sendone(
(self._cr.dbname, "mail.broker", message.channel_id.broker_id.id),
{"message": message.mail_message_id.message_format()[0]},
)
return message

View File

@ -0,0 +1,79 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
from odoo import api, fields, models
class MailChannel(models.Model):
_inherit = "mail.channel"
gateway_channel_token = fields.Char()
anonymous_name = fields.Char() # Same field we will use on im_livechat
gateway_id = fields.Many2one("mail.gateway")
gateway_message_ids = fields.One2many(
"mail.notification",
inverse_name="gateway_channel_id",
)
company_id = fields.Many2one("res.company", default=False)
channel_type = fields.Selection(
selection_add=[("gateway", "Gateway")], ondelete={"gateway": "set default"}
)
gateway_token = fields.Char(
related="gateway_id.token",
string="Gateway related Token",
required=False,
)
def channel_info(self):
result = super().channel_info()
for record, item in zip(self, result):
item["gateway"] = {
"id": record.gateway_id.id,
"name": record.gateway_id.name,
"type": record.gateway_id.gateway_type,
}
item["gateway_name"] = record.gateway_id.name
item["gateway_id"] = record.gateway_id.id
return result
def _generate_avatar_gateway(self):
# We will use this function to set a default avatar on each module
return False
def _generate_avatar(self):
if self.channel_type not in ("gateway"):
return super()._generate_avatar()
avatar = self._generate_avatar_gateway()
if not avatar:
return False
return base64.b64encode(avatar.encode())
@api.returns("mail.message", lambda value: value.id)
def message_post(self, *args, gateway_type=False, **kwargs):
message = super().message_post(
*args, gateway_type=gateway_type or self.gateway_id.gateway_type, **kwargs
)
if (
self.gateway_id
and not self.env.context.get("no_gateway_notification", False)
and message.message_type != "notification"
):
self.env["mail.notification"].create(
{
"mail_message_id": message.id,
"gateway_channel_id": self.id,
"notification_type": "gateway",
"gateway_type": self.gateway_id.gateway_type,
}
).send_gateway()
return message
def _message_update_content_after_hook(self, message):
self.ensure_one()
if self.channel_type == "gateway" and message.gateway_notification_ids:
self.env[
"mail.gateway.{}".format(self.gateway_id.gateway_type)
]._update_content_after_hook(self, message)
return super()._message_update_content_after_hook(message=message)

View File

@ -0,0 +1,149 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command, api, fields, models, tools
class MailGateway(models.Model):
_name = "mail.gateway"
_description = "Mail Gateway"
name = fields.Char(required=True)
token = fields.Char(required=True, help="Key used for integration purposes")
gateway_type = fields.Selection([], required=True)
webhook_key = fields.Char(help="Key used on the connection URL")
webhook_secret = fields.Char(
help="""Key used to ensure that the connection is secure and
comes from the desired source"""
)
integrated_webhook_state = fields.Selection(
[("pending", "Pending"), ("integrated", "Integrated")], readonly=True
)
can_set_webhook = fields.Boolean(compute="_compute_webhook_checks")
webhook_url = fields.Char(compute="_compute_webhook_url")
has_new_channel_security = fields.Boolean(
help="When checked, channels are not created automatically. Usable on Telegram"
)
webhook_user_id = fields.Many2one(
"res.users",
default=lambda self: self.env.user.id,
help="User that will create the messages",
)
member_ids = fields.Many2many(
"res.users", default=lambda self: [Command.link(self.env.user.id)]
)
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company.id
)
_sql_constraints = [
("mail_gateway_token", "unique(token)", "Token must be unique"),
(
"mail_gateway_webhook_key",
"unique(webhook_key)",
"Webhook Key must be unique",
),
]
@api.depends("webhook_key")
def _compute_webhook_url(self):
for record in self:
record.webhook_url = record._get_webhook_url()
def _get_channel_id(self, chat_token):
return (
self.env["mail.channel"]
.search(
[
("gateway_channel_token", "=", str(chat_token)),
("gateway_id", "=", self.id),
],
limit=1,
)
.id
)
def _get_webhook_url(self):
return "%s/gateway/%s/%s/update" % (
self.webhook_url
or self.env["ir.config_parameter"].get_param("web.base.url"),
self.gateway_type,
self.webhook_key,
)
def _can_set_webhook(self):
return self.webhook_key and self.webhook_user_id
@api.depends("gateway_type")
def _compute_webhook_checks(self):
for record in self:
record.can_set_webhook = record._can_set_webhook()
def set_webhook(self):
self.ensure_one()
if self.can_set_webhook:
self.env["mail.gateway.%s" % self.gateway_type]._set_webhook(self)
def remove_webhook(self):
self.ensure_one()
self.env["mail.gateway.%s" % self.gateway_type]._remove_webhook(self)
def update_webhook(self):
self.ensure_one()
self.remove_webhook()
self.set_webhook()
def write(self, vals):
res = super(MailGateway, self).write(vals)
if (
"webhook_key" in vals
or "integrated_webhook_state" in vals
or "webhook_secret" in vals
or "webhook_user_id" in vals
):
self.clear_caches()
return res
@api.model_create_multi
def create(self, mvals):
res = super(MailGateway, self).create(mvals)
self.clear_caches()
return res
@api.model
@tools.ormcache()
def _get_gateway_map(self, state="integrated", gateway_type=False):
result = {}
for record in self.search(
[
("integrated_webhook_state", "=", state),
("gateway_type", "=", gateway_type),
]
):
result[record.webhook_key] = record._get_gateway_data()
return result
def _get_gateway_data(self):
return {
"id": self.id,
"webhook_secret": self.webhook_secret,
"webhook_user_id": self.webhook_user_id.id,
}
@api.model
def _get_gateway(self, key, state="integrated", gateway_type=False):
# We are using cache in order to avoid an exploit
if not key:
return False
return self._get_gateway_map(state=state, gateway_type=gateway_type).get(
key, False
)
def gateway_info(self):
return [record._gateway_info() for record in self]
def _gateway_info(self):
return {
"id": self.id,
"name": self.name,
"type": self.gateway_type,
}

View File

@ -0,0 +1,82 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command, models
class MailGatewayAbstract(models.AbstractModel):
_name = "mail.gateway.abstract"
_description = "Gateway abstract for functions"
def _verify_update(self, bot_data, kwargs):
return True
def _receive_update(self, gateway, kwargs):
pass
def _post_process_message(self, message, channel):
self.env["mail.notification"].search(
[("gateway_channel_id", "=", channel.id), ("is_read", "=", False)]
)._set_read_gateway()
def _post_process_reply(self, related_message):
pass
def _update_content_after_hook(self, channel, message):
pass
def _set_webhook(self, gateway):
gateway.integrated_webhook_state = "integrated"
def _remove_webhook(self, gateway):
gateway.integrated_webhook_state = False
def _get_channel(self, gateway, token, update, force_create=False):
chat_id = gateway._get_channel_id(token)
if chat_id:
return gateway.env["mail.channel"].browse(chat_id)
if not force_create and gateway.has_new_channel_security:
return False
channel = gateway.env["mail.channel"].create(
self._get_channel_vals(gateway, token, update)
)
channel._broadcast(channel.channel_member_ids.mapped("partner_id").ids)
return channel
def _get_author(self, gateway, update):
return False
def _get_channel_vals(self, gateway, token, update):
author = self._get_author(gateway, update)
members = [
Command.create({"partner_id": partner.id, "is_pinned": True})
for partner in gateway.member_ids.partner_id
]
if author:
members.append(
Command.create(
{
"partner_id": author._name == "res.partner" and author.id,
"guest_id": author._name == "mail.guest" and author.id,
}
)
)
return {
"gateway_channel_token": token,
"gateway_id": gateway.id,
"channel_type": "gateway",
"channel_member_ids": members,
"company_id": gateway.company_id.id,
}
def _send(
self,
gateway,
record,
auto_commit=False,
raise_exception=False,
parse_mode=False,
):
raise NotImplementedError()
def _get_message_body(self, record):
return record.mail_message_id.body

View File

@ -0,0 +1,18 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MailGuest(models.Model):
_inherit = "mail.guest"
gateway_id = fields.Many2one("mail.gateway")
gateway_token = fields.Char()
def _guest_format(self, fields=None):
result = super()._guest_format(fields=fields)
if not fields or "gateway_id" in fields:
for guest in result:
result[guest]["gateway"] = {"id": guest.gateway_id.id}
return result

View File

@ -1,6 +1,7 @@
# Copyright 2020 Creu Blanca
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
@ -8,50 +9,115 @@ class MailMessage(models.Model):
_inherit = "mail.message"
broker_channel_id = fields.Many2one(
"mail.channel",
readonly=True,
compute="_compute_broker_channel_id",
store=True,
gateway_type = fields.Selection(
selection=lambda r: r.env["mail.gateway"]._fields["gateway_type"].selection
)
broker_unread = fields.Boolean(default=False)
broker_type = fields.Selection(
selection=lambda r: r.env["mail.broker"]._fields["broker_type"].selection
gateway_notification_ids = fields.One2many(
"mail.notification",
inverse_name="mail_message_id",
domain=[("notification_type", "=", "gateway")],
)
broker_notification_ids = fields.One2many(
"mail.message.broker", inverse_name="mail_message_id"
gateway_channel_ids = fields.Many2many(
"res.partner.gateway.channel", compute="_compute_gateway_channel_ids"
)
gateway_channel_data = fields.Json(compute="_compute_gateway_channel_ids")
gateway_message_ids = fields.One2many(
"mail.message",
inverse_name="gateway_message_id",
string="Child gateway messages",
)
gateway_message_id = fields.Many2one(
"mail.message", string="Original gateway message"
)
gateway_thread_data = fields.Json(compute="_compute_gateway_thread_data")
@api.depends("broker_notification_ids")
def _compute_broker_channel_id(self):
for rec in self:
if rec.broker_notification_ids:
rec.broker_channel_id = rec.broker_notification_ids[0].channel_id
@api.model
def _message_read_dict_postprocess(self, messages, message_tree):
result = super()._message_read_dict_postprocess(messages, message_tree)
for message_dict in messages:
message_id = message_dict.get("id")
message = message_tree[message_id]
notifications = message.broker_notification_ids
if notifications:
message_dict.update(
@api.depends("gateway_message_id")
def _compute_gateway_thread_data(self):
for record in self:
gateway_thread_data = {}
if record.gateway_message_id:
gateway_thread_data.update(
{
"broker_channel_id": message.broker_channel_id.id,
"broker_type": message.broker_type,
"broker_unread": message.broker_unread,
"customer_status": "sent"
if all(d.state == "sent" for d in notifications)
else "received"
if all(d.state == "received" for d in notifications)
else message_dict.get("customer_status", "error"),
"name": record.gateway_message_id.record_name,
"id": record.gateway_message_id.res_id,
"model": record.gateway_message_id.model,
}
)
record.gateway_thread_data = gateway_thread_data
@api.depends("notification_ids", "gateway_message_ids")
def _compute_gateway_channel_ids(self):
for record in self:
if self.env.user.has_group("mail_gateway.gateway_user"):
channels = record.notification_ids.res_partner_id.gateway_channel_ids.filtered(
lambda r: (r.gateway_token, r.gateway_id.id)
not in [
(
notification.gateway_channel_id.gateway_channel_token,
notification.gateway_channel_id.gateway_id.id,
)
for notification in record.gateway_message_ids.gateway_notification_ids
]
)
else:
channels = self.env["res.partner.gateway.channel"]
record.gateway_channel_ids = channels
record.gateway_channel_data = {
"channels": channels.ids,
"partners": channels.partner_id.ids,
}
@api.depends("gateway_notification_ids")
def _compute_gateway_channel_id(self):
for rec in self:
if rec.gateway_notification_ids:
rec.gateway_channel_id = rec.gateway_notification_ids[
0
].gateway_channel_id
def _get_message_format_fields(self):
result = super()._get_message_format_fields()
result.append("gateway_type")
result.append("gateway_channel_data")
result.append("gateway_thread_data")
return result
def set_message_done(self):
# We need to set it as sudo in order to avoid collateral damages.
# In fact, it is done with sudo on the original method
self.sudo().filtered(lambda r: r.broker_unread).write({"broker_unread": False})
return super().set_message_done()
def _send_to_gateway_thread(self, gateway_channel_id):
chat_id = gateway_channel_id.gateway_id._get_channel_id(
gateway_channel_id.gateway_token
)
channel = self.env["mail.channel"].browse(chat_id)
channel.message_post(**self._get_gateway_thread_message_vals())
if not self.gateway_type:
self.gateway_type = gateway_channel_id.gateway_id.gateway_type
self.env["mail.notification"].create(
{
"notification_status": "sent",
"mail_message_id": self.id,
"gateway_channel_id": channel.id,
"notification_type": "gateway",
"gateway_type": gateway_channel_id.gateway_id.gateway_type,
}
)
self.env["bus.bus"]._sendone(
self.env.user.partner_id,
"mail.message/insert",
{
"id": self.id,
"gateway_type": self.gateway_type,
"notifications": self.sudo()
.notification_ids._filtered_for_web_client()
._notification_format(),
},
)
return {}
def _get_gateway_thread_message_vals(self):
return {
"body": self.body,
"attachment_ids": self.attachment_ids.ids,
"subtype_id": self.subtype_id.id,
"author_id": self.env.user.partner_id.id,
"gateway_message_id": self.id,
"message_type": "comment",
}

View File

@ -1,76 +0,0 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class MailMessageBroker(models.Model):
_name = "mail.message.broker"
_description = "Broker Message"
_inherits = {"mail.message": "mail_message_id"}
_order = "id desc"
_rec_name = "subject"
# content
mail_message_id = fields.Many2one(
"mail.message",
"Mail Message",
required=True,
ondelete="cascade",
index=True,
auto_join=True,
)
message_id = fields.Char(readonly=True)
channel_id = fields.Many2one("mail.channel", required=True, ondelete="cascade")
state = fields.Selection(
[
("outgoing", "Outgoing"),
("sent", "Sent"),
("exception", "Delivery Failed"),
("cancel", "Cancelled"),
("received", "Received"),
],
"Status",
readonly=True,
copy=False,
default="outgoing",
)
failure_reason = fields.Text(
"Failure Reason",
readonly=1,
help="Failure reason. This is usually the exception thrown by the"
" email server, stored to ease the debugging of mailing issues.",
)
@api.model_create_multi
def create(self, vals_list):
messages = super().create(vals_list)
if self.env.context.get("notify_broker", False):
notifications = []
for message in messages:
notifications += message.channel_id._channel_message_notifications(
message.mail_message_id
)
self.env["bus.bus"].sudo().sendmany(notifications)
return messages
def send(self, auto_commit=False, raise_exception=False, parse_mode="HTML"):
for record in self:
broker = record.channel_id.broker_id
with broker.work_on(broker._name) as work:
work.component(usage=broker.broker_type)._send(
record,
auto_commit=auto_commit,
raise_exception=raise_exception,
parse_mode=parse_mode,
)
def mark_outgoing(self):
return self.write({"state": "outgoing"})
def cancel(self):
return self.write({"state": "cancel"})

View File

@ -0,0 +1,43 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MailNotification(models.Model):
_inherit = "mail.notification"
gateway_channel_id = fields.Many2one("mail.channel")
notification_type = fields.Selection(
selection_add=[("gateway", "Gateway")], ondelete={"gateway": "cascade"}
)
gateway_message_id = fields.Char(readonly=True)
gateway_failure_reason = fields.Text(
readonly=1,
help="Failure reason. This is usually the exception thrown by the"
" email server, stored to ease the debugging of mailing issues.",
)
gateway_type = fields.Selection(
selection=lambda r: r.env["mail.gateway"]._fields["gateway_type"].selection
)
def _set_read_gateway(self):
self.sudo().write({"is_read": True, "read_date": fields.Datetime.now()})
def _notification_format(self):
result = super()._notification_format()
for record, formatted_value in zip(self, result):
formatted_value["gateway_type"] = record.gateway_type
formatted_value["channel_name"] = record.gateway_channel_id.name
return result
def send_gateway(self, auto_commit=False, raise_exception=False, parse_mode="HTML"):
for record in self:
gateway = record.gateway_channel_id.gateway_id
self.env["mail.gateway.%s" % gateway.gateway_type]._send(
gateway,
record,
auto_commit=auto_commit,
raise_exception=raise_exception,
parse_mode=parse_mode,
)

View File

@ -0,0 +1,85 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
def _notify_thread_by_email(self, message, recipients_data, **kwargs):
partners_data = [r for r in recipients_data if r["notif"] == "gateway"]
if partners_data:
self._notify_thread_by_gateway(message, partners_data, **kwargs)
return super()._notify_thread_by_email(message, recipients_data, **kwargs)
def _notify_thread_by_gateway(self, message, partners_data, **kwargs):
for partner_data in partners_data:
if partner_data["notif"] != "gateway" or not partner_data.get(
"gateway_channel_id"
):
continue
message._send_to_gateway_thread(
self.env["res.partner.gateway.channel"].browse(
partner_data.get("gateway_channel_id")
)
)
def _notify_get_recipients(self, message, msg_vals, **kwargs):
if "gateway_notifications" in kwargs:
result = []
for notification in kwargs["gateway_notifications"]:
if not notification.get("channel_type"):
continue
partner = self.env["res.partner"].browse(notification["partner_id"])
user = partner.user_ids
follower_data = {
"active": partner.active,
"id": partner.id,
"is_follower": True,
"lang": partner.lang,
"groups": set(user.groups_id.ids),
"notif": notification.get("channel_type"),
"share": partner.partner_share,
"uid": user[:1].id,
"ushare": user and any(user.mapped("share")),
"gateway_channel_id": notification.get("gateway_channel_id"),
}
if follower_data["ushare"]: # any type of share user
follower_data["type"] = "portal"
elif follower_data[
"share"
]: # no user, is share -> customer (partner only)
follower_data["type"] = "customer"
else: # has a user not share -> internal user
follower_data["type"] = "user"
result.append(follower_data)
return result
return super()._notify_get_recipients(message, msg_vals, **kwargs)
def _check_can_update_message_content(self, messages):
# We can delete the messages comming from a gateway on not channels
if self._name != "mail.channel":
new_messages = messages.filtered(lambda r: not r.gateway_message_ids)
else:
new_messages = messages
return super()._check_can_update_message_content(new_messages)
def _message_update_content(
self, message, body, attachment_ids=None, strict=True, **kwargs
):
result = super()._message_update_content(
message, body, attachment_ids=attachment_ids, strict=strict, **kwargs
)
if body == "":
# Unlink the message
for gateway_message in message.gateway_message_ids:
gateway_message.gateway_message_id = False
self.env["bus.bus"]._sendone(
self.env.user.partner_id,
"mail.message/insert",
{
"id": gateway_message.id,
"gateway_thread_data": gateway_message.sudo().gateway_thread_data,
},
)
return result

View File

@ -0,0 +1,89 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResPartner(models.Model):
"""Update of res.partner class to take into account the gateway."""
_inherit = "res.partner"
gateway_channel_ids = fields.One2many(
"res.partner.gateway.channel", inverse_name="partner_id"
)
def mail_partner_format(self, fields=None):
"""Override to add gateway info."""
partners_format = super().mail_partner_format(fields=fields)
if not fields:
fields = {"gateway_channel_ids": True}
for partner in self:
if "gateway_channel_ids" in fields:
partners_format.get(partner).update(
{
"gateway_channels": partner.gateway_channel_ids.mail_format(),
}
)
return partners_format
def _get_channels_as_member(self):
channels = super()._get_channels_as_member()
if self.env.user.has_group("mail_gateway.gateway_user"):
channels |= self.env["mail.channel"].search(
[
("channel_type", "=", "gateway"),
(
"channel_member_ids",
"in",
self.env["mail.channel.member"]
.sudo()
._search(
[
("partner_id", "=", self.id),
("is_pinned", "=", True),
]
),
),
]
)
return channels
class ResPartnerGatewayChannel(models.Model):
_name = "res.partner.gateway.channel"
_description = "Technical data used to get the gateway author"
name = fields.Char(related="gateway_id.name")
partner_id = fields.Many2one(
"res.partner", required=True, readonly=True, ondelete="cascade"
)
gateway_id = fields.Many2one(
"mail.gateway", required=True, readonly=True, ondelete="cascade"
)
gateway_token = fields.Char(readonly=True)
company_id = fields.Many2one(
"res.company", related="gateway_id.company_id", store=True
)
_sql_constraints = [
(
"unique_partner_gateway",
"UNIQUE(partner_id, gateway_id)",
"Partner can only have one configuration for each gateway.",
),
]
def mail_format(self):
return [r._mail_format() for r in self]
def _mail_format(self):
return {
"id": self.id,
"name": self.name,
"gateway": {
"id": self.gateway_id.id,
"name": self.gateway_id.name,
"type": self.gateway_id.gateway_type,
},
}

View File

@ -0,0 +1,16 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = "res.users"
gateway_ids = fields.Many2many("mail.gateway")
def _init_messaging(self):
result = super()._init_messaging()
result["gateways"] = self.gateway_ids.gateway_info()
return result

View File

@ -0,0 +1,4 @@
- Access in development mode
- Go to `Settings / Technical / Email / Gateway`
- Create a gateway. Follow the instruction of the specific tab in order to integrate it.
- Start receiving notifications

View File

@ -1,2 +1,2 @@
* Enric Tobella <etobella@creublanca.es>
* Olga Marco <olga.marco@creublanca.es>
* Enric Tobella
* Olga Marco

View File

@ -0,0 +1 @@
This work has been funded by AEOdoo (Asociación Española de Odoo - https://www.aeodoo.org)

View File

@ -1,7 +1,5 @@
This module allows to respond chats as a bot.
This module will allow you to integrate an external chat system in your Odoo system.
It requires extra modules with the specific configuration of each chat system, like `mail_gateway_telegram` or `mail_gateway_whatsapp`.
This way, a group of users can respond customers or any other set
of partners in an integrated way.
It is not intended to be integrated on default chatter as users don't need
to review again when one has responded.
of partners within Odoo, but the messages will be sent through the external chat system.

View File

@ -0,0 +1,9 @@
When external messages are received, they will be directly sent to the discuss menu.
Answering to these messages will send the answer to the external contact.
We can assign this messages to any record using the message actions.
Also, we can assign the sender to a partner using the followers menu and selecting the partner.
On a standard record associated to a partner with external chat, we can send messages to the external contact directly selecting the methods of the partner.
To use this, we just need to use the
It is recomended to enable chatter notification to all users that will receive messages from gateways.

View File

@ -1,7 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_message_broker_all,mail.message.broker.all,model_mail_message_broker,,1,0,0,0
access_mail_message_broker_portal,mail.message.broker.portal,model_mail_message_broker,base.group_portal,1,1,1,0
access_mail_message_broker_user,mail.message.broker.user,model_mail_message_broker,base.group_user,1,1,1,0
access_mail_message_broker_system,mail.message.broker.system,model_mail_message_broker,base.group_system,1,1,1,1
access_mail_broker_all,mail.telegram.bot.all,model_mail_broker,,1,0,0,0
access_mail_broker_system,mail_broker,model_mail_broker,base.group_system,1,1,1,1
access_res_partner_gateway_channel_portal,res.partner.gateway.channel.portal,model_res_partner_gateway_channel,base.group_portal,1,0,0,0
access_res_partner_gateway_channel_user,res.partner.gateway.channel,model_res_partner_gateway_channel,base.group_user,1,0,0,0
manage_res_partner_gateway_channel_user,res.partner.gateway.channel,model_res_partner_gateway_channel,gateway_user,1,1,1,1
access_mail_message_gateway_send_user,mail.message.gateway.send,model_mail_message_gateway_send,base.group_user,1,1,1,0
access_mail_gateway_all,mail.telegram.bot.all,model_mail_gateway,,1,0,0,0
access_mail_guest_manage,mail.telegram.bot.all,model_mail_guest_manage,base.group_user,1,1,1,1
access_mail_message_gateway_link,mail.message.link.all,model_mail_message_gateway_link,base.group_user,1,1,1,1
access_mail_gateway_system,mail_gateway,model_mail_gateway,base.group_system,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mail_message_broker_all access_res_partner_gateway_channel_portal mail.message.broker.all res.partner.gateway.channel.portal model_mail_message_broker model_res_partner_gateway_channel base.group_portal 1 0 0 0
3 access_mail_message_broker_portal access_res_partner_gateway_channel_user mail.message.broker.portal res.partner.gateway.channel model_mail_message_broker model_res_partner_gateway_channel base.group_portal base.group_user 1 1 0 1 0 0
4 access_mail_message_broker_user manage_res_partner_gateway_channel_user mail.message.broker.user res.partner.gateway.channel model_mail_message_broker model_res_partner_gateway_channel base.group_user gateway_user 1 1 1 0 1
5 access_mail_message_broker_system access_mail_message_gateway_send_user mail.message.broker.system mail.message.gateway.send model_mail_message_broker model_mail_message_gateway_send base.group_system base.group_user 1 1 1 1 0
6 access_mail_broker_all access_mail_gateway_all mail.telegram.bot.all model_mail_broker model_mail_gateway 1 0 0 0
7 access_mail_broker_system access_mail_guest_manage mail_broker mail.telegram.bot.all model_mail_broker model_mail_guest_manage base.group_system base.group_user 1 1 1 1
8 access_mail_message_gateway_link mail.message.link.all model_mail_message_gateway_link base.group_user 1 1 1 1
9 access_mail_gateway_system mail_gateway model_mail_gateway base.group_system 1 1 1 1

View File

@ -1,31 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.module.category" id="module_category_broker">
<field name="name">Broker</field>
<record model="ir.module.category" id="module_category_gateway">
<field name="name">Gateway</field>
</record>
<record model="res.groups" id="broker_user">
<record model="res.groups" id="gateway_user">
<field name="name">User</field>
<field name="category_id" ref="module_category_broker" />
<field name="category_id" ref="module_category_gateway" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="mail_channel_broker_rule" model="ir.rule">
<field name="name">Mail.channel: access broker</field>
<record id="mail_channel_gateway_rule" model="ir.rule">
<field name="name">Mail.channel: access gateway</field>
<field name="model_id" ref="mail.model_mail_channel" />
<field name="groups" eval="[(4, ref('mail_broker.broker_user'))]" />
<field name="domain_force">[('public', '=', 'broker')]</field>
<field name="groups" eval="[(4, ref('mail_gateway.gateway_user'))]" />
<field
name="domain_force"
>[('channel_type', '=', 'gateway'), '|', ('company_id', '=', False), ('company_id', 'in', company_ids) ]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_unlink" eval="False" />
</record>
<record id="mail_gateway_rule" model="ir.rule">
<field name="name">Mail.gateway: multicompany rule</field>
<field name="model_id" ref="mail_gateway.model_mail_gateway" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids) ]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_unlink" eval="True" />
</record>
<record id="res_partner_gateway_channel_rule" model="ir.rule">
<field name="name">res.partner.gateway.channel: multicompany rule</field>
<field name="model_id" ref="mail_gateway.model_res_partner_gateway_channel" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids) ]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_unlink" eval="True" />
</record>
<record id="ir_rule_mail_channel_partner_group_user" model="ir.rule">
<field
name="name"
>mail.channel.partner: write its own entries on broker channels</field>
<field name="model_id" ref="mail.model_mail_channel_partner" />
<field name="groups" eval="[(4, ref('mail_broker.broker_user'))]" />
<field name="domain_force">[('channel_id.public', '=', 'broker')]</field>
>mail.channel.member: write its own entries on gateway channels members</field>
<field name="model_id" ref="mail.model_mail_channel_member" />
<field name="groups" eval="[(4, ref('mail_gateway.gateway_user'))]" />
<field name="domain_force">[('channel_id.channel_type', '=', 'gateway')]</field>
<field name="perm_read" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="False" />

View File

@ -1 +0,0 @@
from . import mail_broker_service

View File

@ -1,86 +0,0 @@
# Copyright 2022 CreuBlanca
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import AbstractComponent
class BrokerMethodParams(restapi.RestMethodParam):
def from_params(self, service, params):
return params
def to_response(self, service, result):
return result
def to_openapi_requestbody(self, service):
return {"content": {}}
def to_openapi_responses(self, service):
return {"200": {"content": {}}}
def to_openapi_query_parameters(self, service, spec):
return []
def to_json_schema(self, service, spec, direction):
return {}
class MailBrokerService(AbstractComponent):
_inherit = "base.rest.service"
_name = "mail.broker.base.service"
_usage = "broker"
_collection = "mail.broker"
_description = "Mail Broker Services"
@restapi.method(
[(["/<string:bot_key>/update"], "POST")],
# output_param=BrokerMethodParams(),
input_param=BrokerMethodParams(),
auth="none",
)
def post_update(self, token, **kwargs):
"""Post an update from an external service"""
bot_data = self.env["mail.broker"]._get_broker(
token, broker_type=self._usage, state="integrated", **kwargs
)
if not bot_data:
return {}
if not self._verify_update(bot_data, kwargs):
return {}
self.collection.env = self.env(user=bot_data["webhook_user_id"])
broker = self.env["mail.broker"].browse(bot_data["id"])
self._receive_update(broker.with_context(notify_broker=True), kwargs)
return False
def _verify_update(self, bot_data, kwargs):
return True
def _receive_update(self, broker, kwargs):
pass
def _set_webhook(self):
self.collection.integrated_webhook_state = "integrated"
def _remove_webhook(self):
self.collection.integrated_webhook_state = False
def _get_channel(self, broker, token, update, force_create=False):
chat_id = broker._get_channel_id(token)
if chat_id:
return broker.env["mail.channel"].browse(chat_id)
if not force_create and broker.has_new_channel_security:
return False
return broker.env["mail.channel"].create(
self._get_channel_vals(broker, token, update)
)
def _get_channel_vals(self, broker, token, update):
return {
"token": token,
"broker_id": broker.id,
"show_on_app": broker.show_on_app,
"public": "broker",
"channel_type": "broker",
}
def _send(self, record, auto_commit=False, raise_exception=False, parse_mode=False):
raise NotImplementedError()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,160 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="Capa_1"
data-name="Capa 1"
viewBox="0 0 200.33 200"
version="1.1"
sodipodi:docname="icon.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/operador/pyworkspace12/social/mail_broker/static/description/icon.png"
inkscape:export-xdpi="95.841858"
inkscape:export-ydpi="95.841858">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>icon</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1853"
inkscape:window-height="1025"
id="namedview22"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-text-baseline="true"
inkscape:zoom="1.668772"
inkscape:cx="32.080636"
inkscape:cy="54.692004"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer2">
<sodipodi:guide
position="51.271186,126.37712"
orientation="-0.70710678,0.70710678"
id="guide40"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="118.65012,168.08767"
orientation="-0.70710678,0.70710678"
id="guide42"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="138.02966,143.00847"
orientation="-0.70710678,0.70710678"
id="guide46"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="137.92373,118.22034"
orientation="-0.70710678,0.70710678"
id="guide48"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="156.99152,93.008474"
orientation="-0.70710678,0.70710678"
id="guide50"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="139.19491,46.822034"
orientation="-0.70710678,0.70710678"
id="guide52"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="33.070725,104.38065"
orientation="-0.70710678,0.70710678"
id="guide839"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="136.62741,46.516241"
orientation="-0.70710678,0.70710678"
id="guide841"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<defs
id="defs4">
<style
id="style2">.cls-1{fill:none;}.cls-2{fill:#3b588f;}.cls-3{fill:#070308;opacity:0.4;}.cls-4{fill:#fff;}</style>
</defs>
<title
id="title6">icon</title>
<rect
id="_Sector_"
data-name="&lt;Sector&gt;"
class="cls-1"
width="200"
height="200" />
<rect
class="cls-2"
x="0.33000001"
width="200"
height="200"
id="rect9"
ry="5.6928086"
y="0"
style="fill:#179cde;fill-opacity:1" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Layer 1"
style="display:inline" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 3">
<path
style="fill:#000000;fill-opacity:0.29051986;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 34.149962,94.540113 0.33939645,128.35068 c 0,22.04621 -0.005125,-37.02851 -0.005125,66.40594 0,1.19849 0.87634879,2.80824 1.47559179,3.40749 0,0 1.6732367,1.97005 4.5570938,1.85769 l 83.7317707,-0.009 47.844652,-47.84466 4.88661,-95.023805 z"
id="path843"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 2"
style="display:inline">
<path
id="path828"
d="m 158.96325,57.709612 -19.05983,89.885658 c -1.43794,6.34387 -5.18788,7.92279 -10.51673,4.93412 l -29.04085,-21.4 -14.012923,13.47721 c -1.550725,1.55072 -2.84769,2.84769 -5.836362,2.84769 l 2.086431,-29.57656 53.824254,-48.636368 c 2.34019,-2.08643 -0.50751,-3.24242 -3.63715,-1.15599 L 66.229882,109.98314 37.583764,101.01713 c -6.23109,-1.945458 -6.34388,-6.231088 1.29697,-9.219758 L 150.92767,48.63082 c 5.18788,-1.945453 9.72728,1.155997 8.03558,9.078792 z"
inkscape:connector-curvature="0"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.28195" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Mail Broker</title>
<title>Mail Gateway</title>
<style type="text/css">
/*
@ -360,64 +359,80 @@ ul.auto-toc {
</style>
</head>
<body>
<div class="document" id="mail-broker">
<h1 class="title">Mail Broker</h1>
<div class="document" id="mail-gateway">
<h1 class="title">Mail Gateway</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:1708757421780a7323d97082c1710d228ae9283bf9c9fbc1f5dc35ca1a8381d0
!! source digest: sha256:801096a9a4e9f69df86b66ba9592250e32272c7f301eea5fee5b2824aa5fe175
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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_broker"><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_broker"><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>
<p>This module allows to respond chats as a bot.</p>
<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"><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"><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>
<p>This module will allow you to integrate an external chat system in your Odoo system.
It requires extra modules with the specific configuration of each chat system, like <cite>mail_gateway_telegram</cite> or <cite>mail_gateway_whatsapp</cite>.</p>
<p>This way, a group of users can respond customers or any other set
of partners in an integrated way.</p>
<p>It is not intended to be integrated on default chatter as users dont need
to review again when one has responded.</p>
of partners within Odoo, but the messages will be sent through the external chat system.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>When external messages are received, they will be directly sent to the discuss menu.
Answering to these messages will send the answer to the external contact.
We can assign this messages to any record using the message actions.
Also, we can assign the sender to a partner using the followers menu and selecting the partner.</p>
<p>On a standard record associated to a partner with external chat, we can send messages to the external contact directly selecting the methods of the partner.
To use this, we just need to use the</p>
<p>It is recomended to enable chatter notification to all users that will receive messages from gateways.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
<h1><a class="toc-backref" href="#toc-entry-2">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
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mail_broker%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mail_gateway%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<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-2">Credits</a></h1>
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
<h2><a class="toc-backref" href="#toc-entry-4">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-4">Contributors</a></h2>
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Olga Marco &lt;<a class="reference external" href="mailto:olga.marco&#64;creublanca.es">olga.marco&#64;creublanca.es</a>&gt;</li>
<li>Enric Tobella</li>
<li>Olga Marco</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-6">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-5">Maintainers</a></h2>
<h2><a class="toc-backref" href="#toc-entry-7">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>
<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>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/16.0/mail_broker">OCA/social</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/16.0/mail_gateway">OCA/social</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="mail_gateway.ChatterTopbar"
t-inherit="mail.ChatterTopbar"
t-inherit-mode="extension"
owl="1"
>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonSendMessage')]"
position="attributes"
>
<attribute name="t-att-class">{
'o-active btn-odoo': chatterTopbar.chatter.composerView and !chatterTopbar.chatter.composerView.composer.isLog and !chatterTopbar.chatter.composerView.composer.isGateway,
'btn-odoo': !chatterTopbar.chatter.composerView,
'btn-light': chatterTopbar.chatter.composerView and (chatterTopbar.chatter.composerView.composer.isLog or chatterTopbar.chatter.composerView.composer.isGateway),
}</attribute>
</xpath>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonLogNote')]"
position="after"
>
<button
class="o_ChatterTopbar_button o_ChatterTopbar_buttonLogWhatsapp btn text-nowrap btn-light"
type="button"
t-att-class="{
'o-active btn-odoo': chatterTopbar.chatter.composerView and !chatterTopbar.chatter.composerView.composer.isLog and chatterTopbar.chatter.composerView.composer.isGateway,
'btn-odoo': !chatterTopbar.chatter.composerView,
'btn-light': !chatterTopbar.chatter.composerView or (chatterTopbar.chatter.composerView.composer.isLog or !chatterTopbar.chatter.composerView.composer.isGateway),
}"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
t-on-click="() => this.chatterTopbar.chatter.onClickGatewayMessage()"
>
<i class="fa fa-plane" role="img" aria-label="gateway" />
<span> Gateway message</span>
</button>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="mail_gateway.Composer"
t-inherit="mail.Composer"
t-inherit-mode="extension"
owl="1"
>
<xpath expr="//small[hasclass('o_Composer_followers')]/.." position="after">
<t t-if="composerView.composer.isGateway">
<small><b class="text-muted">To: </b></small>
<t
t-foreach="composerView.composer.composerGatewayFollowers"
t-as="followerGateway"
t-key="followerGateway.follower"
>
<GatewayFollowerView record="followerGateway" />
</t>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="mail.DiscussSidebar" t-inherit-mode="extension">
<xpath expr="//*[@name='beforeCategoryChat']" position="before">
<t
t-foreach="discussView.discuss.categoryGateways"
t-as="categoryGateway"
t-key="categoryGateway"
>
<DiscussSidebarCategory
className="'o_DiscussSidebar_category o_DiscussSidebar_categoryGateway' + categoryGateway.gateway_id"
record="categoryGateway"
/>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,31 @@
/** @odoo-module **/
import {registerMessagingComponent} from "@mail/utils/messaging_component";
import {useComponentToModel} from "@mail/component_hooks/use_component_to_model";
const {Component} = owl;
class GatewayFollowerView extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({fieldName: "component"});
}
get composerGatewayFollower() {
return this.props.record;
}
onChangeGatewayChannel(ev) {
this.props.record.update({
channel: parseInt(ev.target.options[ev.target.selectedIndex].value, 10),
});
}
}
Object.assign(GatewayFollowerView, {
props: {record: Object},
template: "mail_gateway.GatewayFollowerView",
});
registerMessagingComponent(GatewayFollowerView);

View File

@ -0,0 +1,12 @@
.o_gateway_composer_selector {
select {
width: unset;
display: inline-block;
min-width: 15%;
}
.o_gateway_composer_selector_partner {
min-width: 30%;
display: inline-block;
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="mail_gateway.GatewayFollowerView" owl="1">
<div class="o_gateway_composer_selector">
<span
class="o_gateway_composer_selector_partner"
t-esc="composerGatewayFollower.follower.partner.persona.name"
/>
<select
name="gatewayChannel"
class="o_input o_gateway_composer_selector_channel"
t-att-value="composerGatewayFollower.channel"
t-on-change="onChangeGatewayChannel"
>
<option value="">Not selected</option>
<t
t-foreach="composerGatewayFollower.follower.partner.gateway_channels"
t-as="gateway_channel"
t-key="gateway_channel.id"
>
<option
t-att-value="gateway_channel.id"
t-esc="gateway_channel.name"
t-att-selected="gateway_channel.id === composerGatewayFollower.channel"
/>
</t>
</select>
</div>
</t>
</templates>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="mail.Message" t-inherit-mode="extension">
<xpath expr="//small[hasclass('o_Message_originThread')]/.." position="after">
<t t-if="messageView.message.gatewayThread">
<small
class="o_Message_originThread me-2"
t-att-class="{ 'o-message-selected text-600': messageView.isSelected, 'text-500': !messageView.isSelected }"
>
on <a
class="o_Message_originThreadLink fs-6"
t-att-href="messageView.message.gatewayThread.url"
t-on-click="messageView.onClickGatewayThread"
><t t-if="messageView.message.gatewayThread.displayName"><t
t-esc="messageView.message.gatewayThread.displayName"
/></t><t t-else="">document</t></a>
</small>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="mail.MessageNotificationPopoverContent" t-inherit-mode="extension">
<xpath
expr="//div[hasclass('o_MessageNotificationPopoverContent_notification')]"
position="inside"
>
<span
class="o_MessageNotificationPopoverContent_notificationPartnerName"
t-esc="notification.channel_name"
t-if="notification.notification_type == 'gateway'"
/>
</xpath>
</t>
</templates>

View File

@ -1,124 +0,0 @@
odoo.define("mail_broker/static/src/broker.js", function (require) {
"use strict";
const components = {
Discuss: require("mail_broker/static/src/discuss.js"),
};
const AbstractAction = require("web.AbstractAction");
const {action_registry} = require("web.core");
const {Component} = owl;
var Broker = AbstractAction.extend({
template: "mail.widgets.Discuss",
hasControlPanel: false,
loadControlPanel: false,
withSearchBar: false,
searchMenuTypes: ["filter", "favorite"],
/**
* @override {web.AbstractAction}
* @param {web.ActionManager} parent
* @param {Object} action
* @param {Object} [action.context]
* @param {String} [action.context.active_id]
* @param {Object} [action.params]
* @param {String} [action.params.default_active_id]
* @param {Object} [options={}]
*/
init(parent, action, options = {}) {
this._super(...arguments);
// Control panel attributes
this.action = action;
this.actionManager = parent;
this.searchModelConfig.modelName = "mail.message";
this.discuss = undefined;
this.options = options;
this.component = undefined;
this._lastPushStateActiveThread = null;
},
/**
* @override
*/
async willStart() {
await this._super(...arguments);
this.env = Component.env;
await this.env.messagingCreatedPromise;
const initActiveId =
this.options.active_id ||
(this.action.context && this.action.context.active_id) ||
(this.action.params && this.action.params.default_active_id) ||
"mail.box_inbox";
this.discuss = this.env.messaging.discuss;
this.discuss.update({initActiveId});
},
/**
* @override {web.AbstractAction}
*/
destroy() {
if (this.component) {
this.component.destroy();
this.component = undefined;
}
this._super(...arguments);
},
/**
* @override {web.AbstractAction}
*/
on_attach_callback() {
this._super(...arguments);
if (this.component) {
// Prevent twice call to on_attach_callback (FIXME)
return;
}
const DiscussComponent = components.Discuss;
this.component = new DiscussComponent();
this._pushStateActionManagerEventListener = (ev) => {
ev.stopPropagation();
if (this._lastPushStateActiveThread === this.discuss.thread) {
return;
}
this._pushStateActionManager();
this._lastPushStateActiveThread = this.discuss.thread;
};
this.el.addEventListener(
"o-push-state-action-manager",
this._pushStateActionManagerEventListener
);
return this.component.mount(this.el);
},
/**
* @override {web.AbstractAction}
*/
on_detach_callback() {
this._super(...arguments);
if (this.component) {
this.component.destroy();
}
this.component = undefined;
this.el.removeEventListener(
"o-push-state-action-manager",
this._pushStateActionManagerEventListener
);
this._lastPushStateActiveThread = null;
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* @private
*/
_pushStateActionManager() {
this.actionManager.do_push_state({
action: this.action.id,
active_id: this.discuss.activeId,
});
},
});
action_registry.add("mail.broker", Broker);
return Broker;
});

View File

@ -1,85 +0,0 @@
odoo.define("mail_broker/static/src/js/broker_model.js", function (require) {
"use strict";
const {
registerNewModel,
registerInstancePatchModel,
registerFieldPatchModel,
registerClassPatchModel,
} = require("mail/static/src/model/model_core.js");
const {attr, many2one} = require("mail/static/src/model/model_field.js");
function factoryBroker(dependencies) {
class Broker extends dependencies["mail.model"] {}
Broker.modelName = "mail.broker";
Broker.fields = {
id: attr(),
name: attr(),
};
return Broker;
}
registerNewModel("mail.broker", factoryBroker);
registerInstancePatchModel(
"mail.messaging_initializer",
"mail_broker/static/src/js/broker_model.js",
{
async _init({broker_slots}) {
_.each(broker_slots, (broker_slot) =>
this.env.models["mail.broker"].insert(
Object.assign({model: "mail.broker"}, broker_slot)
)
);
return this._super(...arguments);
},
}
);
registerInstancePatchModel(
"mail.discuss",
"mail_broker/static/src/js/broker_model.js",
{
async openBrokerChannel(thread) {
this.update({
brokerChannel: [["link", thread]],
});
this.focus();
this.env.bus.trigger("do-action", {
action: "mail_broker.mail_broker_action_window",
options: {
active_id: this.threadToActiveId(this),
clear_breadcrumbs: false,
on_reverse_breadcrumb: () => this.close(),
},
});
},
}
);
registerClassPatchModel(
"mail.thread",
"mail_broker/static/src/js/broker_model.js",
{
convertData(data) {
const data2 = this._super(data);
data2.broker_id = data.broker_id;
data2.broker_unread_counter = data.broker_unread_counter;
return data2;
},
}
);
registerFieldPatchModel(
"mail.discuss",
"mail_broker/static/src/js/broker_model.js",
{
brokerChannel: many2one("mail.thread"),
}
);
registerFieldPatchModel(
"mail.thread",
"mail_broker/static/src/js/broker_model.js",
{
broker_id: attr(),
broker_unread_counter: attr(),
}
);
});

View File

@ -1,69 +0,0 @@
odoo.define("mail_broker/static/src/discuss.js", function (require) {
"use strict";
const Discuss = require("mail/static/src/components/discuss/discuss.js");
const DiscussSidebar = require("mail/static/src/components/discuss_sidebar/discuss_sidebar.js");
const DiscussSidebarItem = require("mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js");
const {Component} = owl;
class BrokerDiscussSidebarItem extends DiscussSidebarItem {
get counter() {
if (this.thread.channel_type === "broker") {
return this.thread.broker_unread_counter;
}
return super.counter;
}
}
class DiscussSidebarBroker extends Component {
constructor(...args) {
super(...args);
this.mailBroker = this.env.models["mail.broker"].get(this.props.brokerId);
}
get quickSearchOrderedAndPinnedBrokerChannels() {
return this.env.models["mail.thread"]
.all((channel) => channel.broker_id === this.mailBroker.id)
.sort((c1, c2) => {
if (!c1.lastMessage && !c2.lastMessage) {
return c1.id < c2.id;
} else if (!c2.lastMessage) {
return -1;
} else if (!c1.lastMessage) {
return 1;
}
return c1.lastMessage.id < c2.lastMessage.id ? -1 : 1;
});
}
}
Object.assign(DiscussSidebarBroker, {
components: Object.assign(DiscussSidebar.components, {
DiscussSidebarItem: BrokerDiscussSidebarItem,
}),
props: {
brokerId: String,
},
template: "mail_broker.DiscussSidebarBroker",
});
class BrokerDiscussSidebar extends DiscussSidebar {
get mailBrokers() {
return this.env.models["mail.broker"].all();
}
}
Object.assign(BrokerDiscussSidebar, {
components: Object.assign(DiscussSidebar.components, {DiscussSidebarBroker}),
props: DiscussSidebar.props,
template: "mail_broker.DiscussSidebar",
});
class NewDiscuss extends Discuss {}
Object.assign(NewDiscuss, {
components: Object.assign({}, Discuss.components, {
DiscussSidebar: BrokerDiscussSidebar,
}),
props: Discuss.props,
template: Discuss.template,
});
return NewDiscuss;
});

View File

@ -1,85 +0,0 @@
odoo.define("mail_broker.mail.Manager", function (require) {
"use strict";
var Manager = require("mail.Manager");
var BrokerThread = require("mail_broker.BrokerThread");
Manager.include({
_addMessageToThreads: function (message, options) {
this._super.apply(this, arguments);
if (message.broker_channel_id) {
var thread = this.getThread(
"broker_thread_" + message.broker_channel_id
);
if (thread) {
thread.addMessage(message, options);
}
}
},
_updateInternalStateFromServer: function (result) {
this._super.apply(this, arguments);
this._updateBrokerChannelFromServer(result);
},
getBrokerBots: function () {
var data = _.extend({}, this._broker_bots);
_.each(data, function (value) {
value.threads = [];
});
_.each(this._threads, function (thread) {
if (thread.getType() === "broker_thread") {
data[thread.broker_id].threads.push(thread);
}
});
_.each(data, function (value) {
value.threads.sort(function (a, b) {
return b.last_message_date - a.last_message_date;
});
});
return data;
},
_updateBrokerChannelFromServer: function (data) {
var self = this;
this._broker_bots = {};
_.each(data.broker_slots, function (slot) {
self._broker_bots[slot.id] = {
name: slot.name,
channel_name: slot.channel_name,
};
_.each(slot.threads, self._addChannel.bind(self));
});
},
getMailBrokerThreads: function () {
var data = _.filter(this._threads, function (thread) {
return thread.getType() === "broker_thread";
});
data = data.sort(function (a, b) {
return b.last_message_date - a.last_message_date;
});
return data;
},
_makeChannel: function (data, options) {
if (data.channel_type === "broker_thread") {
return new BrokerThread({
parent: this,
data: data,
options: options,
commands: this._commands,
});
}
return this._super.apply(this, arguments);
},
_onNotification: function (notifs) {
var self = this;
var result = this._super.apply(this, arguments);
_.each(notifs, function (notif) {
if (notif[0][1] === "mail.broker") {
if (notif[1].message) {
self.addMessage(notif[1].message, {silent: 0});
} else if (notif[1].thread) {
self._addChannel(notif[1].thread);
}
}
});
return result;
},
});
});

View File

@ -1,23 +0,0 @@
odoo.define("mail_broker/static/src/js/message.js", function (require) {
"use strict";
const {registerClassPatchModel} = require("mail/static/src/model/model_core.js");
registerClassPatchModel(
"mail.message",
"mail/static/src/models/message/message.js",
{
convertData(data) {
const data2 = this._super(data);
if ("broker_channel_id" in data) {
data2.broker_channel_id = data.broker_channel_id || false;
data2.broker_unread = data.broker_unread || false;
data2.broker_type = data.broker_type || false;
data2.customer_status = data.customer_status || false;
data2.isNeedaction = data2.isNeedaction || data.broker_unread;
}
return data2;
},
}
);
});

View File

@ -1,118 +0,0 @@
odoo.define("mail_broker.BrokerThread", function (require) {
"use strict";
var Thread = require("mail.model.Thread");
var session = require("web.session");
var field_utils = require("web.field_utils");
var BrokerThread = Thread.extend({
init: function (params) {
this._messageIDs = [];
var data = params.data;
data.type = "broker_thread";
this.resId = data.res_id;
this._super.apply(this, arguments);
this._messages = [];
this.last_message_date = field_utils.parse.datetime(data.last_message_date);
this.allHistoryLoaded = false;
this.broker_id = data.broker_id;
this._unreadCounter = data.unread;
},
getMessages: function () {
return this._messages;
},
getLastSeenMessageID: function () {
return null;
},
getNeedactionCounter: function () {
return this._unreadCounter;
},
isGroupBasedSubscription: function () {
return true;
},
_addMessage: function (message) {
this._super.apply(this, arguments);
if (_.contains(this._messages, message)) {
return;
}
// Update internal list of messages
this._messages.push(message);
this._messages = _.sortBy(this._messages, function (msg) {
return msg.getID();
});
// Update message ids associated to this document thread
if (!_.contains(this._messageIDs, message.getID())) {
this._messageIDs.push(message.getID());
}
if (message._date > this.last_message_date) {
this.last_message_date = message._date;
}
if (message.isNeedaction()) {
this._unreadCounter++;
}
},
isAllHistoryLoaded: function () {
return this.allHistoryLoaded;
},
fetchMessages: function (options) {
return this._fetchMessages(options);
},
_fetchMessages: function (options) {
var self = this;
var domain = [];
if (options && options.loadMore) {
var minMessageID = this._messages[0].getID();
domain = [["id", "<", minMessageID]].concat(domain);
}
return this._rpc({
model: "mail.channel",
method: "message_fetch",
args: [[this.resId], domain],
kwargs: this._getFetchMessagesKwargs(options),
}).then(function (messages) {
if (!self.allHistoryLoaded) {
self.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
}
_.each(messages, function (messageData) {
self.call("mail_service", "addMessage", messageData, {
silent: true,
});
});
});
},
_getFetchMessagesKwargs: function () {
return {
limit: this._FETCH_LIMIT,
context: session.user_context,
};
},
_postMessage: function (data) {
var self = this;
return this._super.apply(this, arguments).then(function (messageData) {
_.extend(messageData, {
broker_type: "comment",
subtype: "mail.mt_comment",
command: data.command,
});
return self
._rpc({
model: "mail.broker.channel",
method: "broker_message_post",
args: [[self.resId]],
kwargs: messageData,
})
.then(function () {
return messageData;
});
});
},
_markAsRead: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.call("mail_service", "markMessagesAsRead", self._messageIDs);
});
},
});
return BrokerThread;
});

View File

@ -0,0 +1,33 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Channel",
fields: {
discussSidebarCategory: {
compute() {
// On gateway channels we must set the right category
if (this.thread && this.thread.gateway) {
const category = this.messaging.discuss.categoryGateways.filter(
(ctg) => ctg.gateway === this.thread.gateway
);
if (category.length > 0) {
return category[0];
}
}
return this._super();
},
},
correspondent: {
compute() {
// We will not set a correspondent on gateways, as it gets yourself.
if (this.channel_type === "gateway") {
return clear();
}
return this._super();
},
},
},
});

View File

@ -0,0 +1,39 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "ChannelMemberView",
recordMethods: {
onClickMember(ev) {
if (
this.channelMember.persona.guest &&
this.channelMember.persona.guest.gateway
) {
ev.stopPropagation();
return this.env.services.action.doAction({
name: this.env._t("Manage guest"),
type: "ir.actions.act_window",
res_model: "mail.guest.manage",
context: {default_guest_id: this.channelMember.persona.guest.id},
views: [[false, "form"]],
target: "new",
});
}
return this._super();
},
},
fields: {
hasOpenChat: {
compute() {
return (
this._super() ||
Boolean(
this.channelMember.persona.guest &&
this.channelMember.persona.guest.gateway
)
);
},
},
},
});

View File

@ -0,0 +1,41 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Chatter",
recordMethods: {
onClickLogNote() {
if (this.composerView && this.composerView.composer.isGateway) {
this.update({composerView: clear()});
}
this._super(...arguments);
if (this.composerView) {
this.composerView.composer.update({isGateway: false});
}
},
onClickSendMessage() {
if (this.composerView && this.composerView.composer.isGateway) {
this.update({composerView: clear()});
}
this._super(...arguments);
if (this.composerView) {
this.composerView.composer.update({isGateway: false});
}
},
onClickGatewayMessage() {
if (this.composerView && this.composerView.composer.isGateway) {
this.update({composerView: clear()});
} else {
this.showGatewayComposerView();
}
},
showGatewayComposerView() {
this.update({composerView: {}});
this.composerView.composer.update({isLog: false, isGateway: true});
this.focus();
},
},
fields: {},
});

View File

@ -0,0 +1,32 @@
/** @odoo-module **/
import {attr, many} from "@mail/model/model_field";
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Composer",
fields: {
isGateway: attr({
default: false,
}),
composerGatewayFollowers: many("composerGatewayFollower", {
compute() {
if (this.thread && this.isGateway) {
return this.thread.followers
.filter(
(follower) => follower.partner.gateway_channels.length > 0
)
.map((follower) => {
return {
follower,
channel: follower.partner.gateway_channels[0].id,
};
});
}
return clear();
},
inverse: "composer",
}),
},
});

View File

@ -0,0 +1,32 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {registerModel} from "@mail/model/model_core";
registerModel({
name: "composerGatewayFollower",
recordMethods: {
_getMessageData() {
return {
partner_id: this.follower.partner.id,
channel_type: "gateway",
gateway_channel_id: this.channel,
};
},
},
fields: {
component: attr(),
follower: one("Follower", {identifying: true}),
composer: one("Composer", {
identifying: true,
inverse: "composerGatewayFollowers",
}),
channel_type: attr({}),
channel: attr(),
hasGatewayChannels: attr({
compute() {
return this.follower.partner.gateway_channels.length > 0;
},
}),
},
});

View File

@ -0,0 +1,56 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "ComposerView",
recordMethods: {
_getMessageData() {
var result = this._super(...arguments);
if (this.composer.isGateway) {
result.gateway_notifications =
this.composer.composerGatewayFollowers.map((follower) => {
return follower._getMessageData();
});
}
return result;
},
},
fields: {
hasFollowers: {
compute() {
if (this.composer.isGateway) {
return false;
}
return this._super();
},
},
hasHeader: {
compute() {
return Boolean(this._super() || this.composer.isGateway);
},
},
isExpandable: {
/*
We will not allow to expand on this composer due to all complexity of selection
*/
compute() {
if (this.composer.isGateway) {
return clear();
}
return this._super();
},
},
composerGatewayChannelView: one("GatewayChannelView", {
compute() {
if (this.composer.isGateway) {
return {};
}
return clear();
},
inverse: "composerViewOwner",
}),
},
});

View File

@ -0,0 +1,51 @@
/** @odoo-module **/
import {many} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Discuss",
fields: {
/**
* Discuss sidebar category for `gateway` channel threads.
*/
categoryGateways: many("DiscussSidebarCategory", {
inverse: "discussAsGateways",
}),
},
recordMethods: {
async handleAddGatewayAutocompleteSource(req, res, gateway_id) {
this.discussView.update({addingChannelValue: req.term});
const threads = await this.messaging.models.Thread.searchGatewaysToOpen({
limit: 10,
searchTerm: req.term,
gateway_id,
});
const items = threads.map((thread) => {
const escapedName = escape(thread.name);
return {
id: thread.id,
label: escapedName,
value: escapedName,
gateway_id: gateway_id,
};
});
res(items);
},
async handleAddGatewayAutocompleteSelect(ev, ui, gateway_id) {
// Necessary in order to prevent AutocompleteSelect event's default
// behaviour as html tags visible for a split second in text area
ev.preventDefault();
const channel = this.messaging.models.Thread.insert({
id: ui.item.id,
model: "mail.channel",
gateway_id: gateway_id,
});
await channel.join();
// Channel must be pinned immediately to be able to open it before
// the result of join is received on the bus.
channel.update({isServerPinned: true});
channel.open();
},
},
});

View File

@ -0,0 +1,128 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "DiscussSidebarCategory",
fields: {
gateway_id: attr({identifying: true}),
gateway: one("Gateway", {inverse: "categories"}),
discussAsGateways: one("Discuss", {
inverse: "categoryGateways",
}),
hasAddCommand: {
compute() {
if (this.gateway_id) {
return true;
}
return this._super();
},
},
activeItem: {
compute() {
// We need to adapt this function to refresh the right category only
const channel =
this.messaging.discuss.activeThread &&
this.messaging.discuss.activeThread.channel;
if (
channel &&
this.gateway_id &&
this.supportedChannelTypes.includes(channel.channel_type) &&
channel.thread.gateway_id !== this.gateway_id
) {
return clear();
}
return this._super();
},
},
autocompleteMethod: {
compute() {
if (this.gateway_id) {
return "gateway";
}
return this._super();
},
},
newItemPlaceholderText: {
compute() {
if (this.gateway_id) {
return this.env._t("Find a gateway channel...");
}
return this._super();
},
},
isServerOpen: {
compute() {
// There is no server state for non-users (guests)
if (!this.messaging.currentUser) {
return clear();
}
if (!this.messaging.currentUser.res_users_settings_id) {
return clear();
}
if (this.gateway_id) {
return true;
}
return this._super();
},
},
name: {
compute() {
if (this.gateway_id) {
return this.gateway.name;
}
return this._super();
},
},
categoryItemsOrderedByLastAction: {
compute() {
if (this.gateway_id) {
return this.categoryItems;
}
return this._super();
},
},
orderedCategoryItems: {
compute() {
if (this.gateway_id) {
return this.categoryItemsOrderedByLastAction;
}
return this._super();
},
},
supportedChannelTypes: {
compute() {
if (this.gateway_id) {
return ["gateway"];
}
return this._super();
},
},
},
recordMethods: {
onAddItemAutocompleteSource(req, res) {
if (this.autocompleteMethod === "gateway") {
this.messaging.discuss.handleAddGatewayAutocompleteSource(
req,
res,
this.gateway_id
);
}
return this._super(...arguments);
},
onAddItemAutocompleteSelect(ev, ui) {
if (this.autocompleteMethod === "gateway") {
return this.messaging.discuss.handleAddGatewayAutocompleteSelect(
ev,
ui,
this.gateway_id
);
}
return this._super(...arguments);
},
},
});

View File

@ -0,0 +1,51 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "DiscussSidebarCategoryItem",
fields: {
avatarUrl: {
compute() {
// We will use the avatar provied by the channel by default
if (this.channel.channel_type === "gateway") {
return `/web/image/mail.channel/${this.channel.id}/avatar_128?unique=${this.channel.avatarCacheKey}`;
}
return this._super();
},
},
categoryCounterContribution: {
compute() {
if (this.channel.channel_type === "gateway") {
return this.channel.localMessageUnreadCounter > 0 ? 1 : 0;
}
return this._super();
},
},
counter: {
compute() {
if (this.channel.channel_type === "gateway") {
return this.channel.localMessageUnreadCounter;
}
return this._super();
},
},
hasThreadIcon: {
compute() {
if (this.channel.channel_type === "gateway") {
return clear();
}
return this._super();
},
},
hasUnpinCommand: {
compute() {
if (this.channel.channel_type === "gateway") {
return !this.channel.localMessageUnreadCounter;
}
return this._super();
},
},
},
});

View File

@ -0,0 +1,16 @@
/** @odoo-module **/
import {attr, many} from "@mail/model/model_field";
import {registerModel} from "@mail/model/model_core";
registerModel({
name: "Gateway",
fields: {
id: attr({identifying: true}),
name: attr(),
type: attr(),
categories: many("DiscussSidebarCategory", {
inverse: "gateway",
}),
},
});

View File

@ -0,0 +1,14 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {registerModel} from "@mail/model/model_core";
registerModel({
name: "GatewayChannel",
fields: {
id: attr({identifying: true}),
name: attr(),
gateway: one("Gateway"),
partner: one("Partner", {inverse: "gateway_channels"}),
},
});

View File

@ -0,0 +1,15 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {registerModel} from "@mail/model/model_core";
registerModel({
name: "GatewayChannelView",
fields: {
component: attr(),
composerViewOwner: one("ComposerView", {
identifying: true,
inverse: "composerGatewayChannelView",
}),
},
});

View File

@ -0,0 +1,10 @@
/** @odoo-module **/
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Guest",
fields: {
gateway: one("Gateway"),
},
});

View File

@ -0,0 +1,76 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {clear} from "@mail/model/model_field_command";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Message",
fields: {
gateway_type: attr(),
gateway_channel_data: attr(),
gateway_thread_data: attr(),
gatewayThread: one("Thread", {
compute() {
if (
this.gateway_thread_data &&
Object.keys(this.gateway_thread_data).length > 0
) {
return this.gateway_thread_data;
}
return clear();
},
inverse: "messagesAsGatewayThread",
}),
canBeDeleted: {
compute() {
if (
this.originThread &&
this.originThread.model !== "mail.channel" &&
this.gateway_type
) {
return true;
}
if (
this.originThread &&
this.originThread.model === "mail.channel" &&
this.gateway_type
) {
return false;
}
return this._super();
},
},
},
modelMethods: {
convertData(data) {
const data2 = this._super(data);
data2.gateway_type = data.gateway_type;
data2.gateway_channel_data = data.gateway_channel_data;
data2.gateway_thread_data = data.gateway_thread_data;
return data2;
},
},
recordMethods: {
/**
* @private
*/
_computeGatewayData() {
if (
this.gateway_thread_data &&
Object.keys(this.gateway_thread_data).length > 0
) {
this.update({gatewayThread: this.gateway_thread_data});
} else {
this.update({gatewayThread: clear()});
}
},
},
onChanges: [
{
dependencies: ["gateway_thread_data"],
methodName: "_computeGatewayData",
},
],
});

View File

@ -0,0 +1,45 @@
/** @odoo-module **/
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageAction",
fields: {
messageActionListOwnerAsSendGateway: one("MessageActionList", {
identifying: true,
inverse: "actionSendGateway",
}),
messageActionListOwnerAsAddToThread: one("MessageActionList", {
identifying: true,
inverse: "actionAddToThread",
}),
sequence: {
compute() {
if (
this.messageActionListOwner ===
this.messageActionListOwnerAsSendGateway
) {
return 7;
}
if (
this.messageActionListOwner ===
this.messageActionListOwnerAsAddToThread
) {
return 8;
}
return this._super();
},
},
messageActionListOwner: {
compute() {
if (this.messageActionListOwnerAsSendGateway) {
return this.messageActionListOwnerAsSendGateway;
}
if (this.messageActionListOwnerAsAddToThread) {
return this.messageActionListOwnerAsAddToThread;
}
return this._super();
},
},
},
});

View File

@ -0,0 +1,37 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageActionList",
fields: {
actionSendGateway: one("MessageAction", {
compute() {
if (
this.message &&
this.message.gateway_channel_data &&
this.message.gateway_channel_data.partners &&
Object.keys(this.message.gateway_channel_data.partners).length
) {
return {};
}
return clear();
},
inverse: "messageActionListOwnerAsSendGateway",
}),
actionAddToThread: one("MessageAction", {
compute() {
if (
this.message.gateway_type &&
!this.message.gatewayThread &&
this.message.originThread.model === "mail.channel"
) {
return {};
}
return clear();
},
inverse: "messageActionListOwnerAsAddToThread",
}),
},
});

View File

@ -0,0 +1,91 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageActionView",
recordMethods: {
onClick(ev) {
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsSendGateway
) {
ev.stopPropagation();
this.env.services.action.doAction({
name: this.env._t("Send with gateway"),
type: "ir.actions.act_window",
res_model: "mail.message.gateway.send",
context: {
...this.messageAction.messageActionListOwner.message
.gateway_channel_data,
default_message_id:
this.messageAction.messageActionListOwner.message.id,
},
views: [[false, "form"]],
target: "new",
});
return;
}
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsAddToThread
) {
ev.stopPropagation();
this.env.services.action.doAction({
name: this.env._t("Link Message to thread"),
type: "ir.actions.act_window",
res_model: "mail.message.gateway.link",
context: {
default_message_id:
this.messageAction.messageActionListOwner.message.id,
},
views: [[false, "form"]],
target: "new",
});
return;
}
return this._super(...arguments);
},
},
fields: {
title: {
compute() {
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsSendGateway
) {
return this.env._t("Send with gateway");
}
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsAddToThread
) {
return this.env._t("Link to thread");
}
return this._super();
},
},
classNames: {
compute() {
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsSendGateway
) {
return (
this.paddingClassNames +
" fa fa-lg fa-share-square-o o_MessageActionView_actionSendGateway"
);
}
if (
this.messageAction.messageActionListOwner ===
this.messageAction.messageActionListOwnerAsAddToThread
) {
return (
this.paddingClassNames +
" fa fa-lg fa-link o_MessageActionView_actionAddToThread"
);
}
return this._super();
},
},
},
});

View File

@ -0,0 +1,13 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageView",
recordMethods: {
onClickGatewayThread(ev) {
ev.preventDefault();
this.message.gatewayThread.open();
},
},
});

View File

@ -0,0 +1,24 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessagingInitializer",
recordMethods: {
async _init({gateways}) {
const discuss = this.messaging.discuss;
if (gateways) {
this.messaging.executeGracefully(
gateways.map((gatewayData) => () => {
this.messaging.models.DiscussSidebarCategory.insert({
discussAsGateways: discuss,
gateway: gatewayData,
gateway_id: gatewayData.id,
});
})
);
}
this._super(...arguments);
},
},
});

View File

@ -0,0 +1,20 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Notification",
modelMethods: {
convertData(data) {
var data2 = this._super(data);
data2.gateway_type = data.gateway_type;
data2.channel_name = data.channel_name;
return data2;
},
},
fields: {
channel_name: attr(),
gateway_type: attr(),
},
});

View File

@ -0,0 +1,11 @@
/** @odoo-module **/
import {many} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Partner",
fields: {
gateway_channels: many("GatewayChannel", {inverse: "partner"}),
},
});

View File

@ -0,0 +1,77 @@
/** @odoo-module **/
import {many, one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Thread",
fields: {
gateway: one("Gateway"),
messagesAsGatewayThread: many("Message", {
inverse: "gatewayThread",
isCausal: true,
}),
hasInviteFeature: {
compute() {
if (this.channel && this.channel.channel_type === "gateway") {
return true;
}
return this._super();
},
},
hasMemberListFeature: {
compute() {
if (this.channel && this.channel.channel_type === "gateway") {
return true;
}
return this._super();
},
},
isChatChannel: {
compute() {
if (this.channel && this.channel.channel_type === "gateway") {
return true;
}
return this._super();
},
},
},
modelMethods: {
convertData(data) {
var data2 = this._super(data);
if (data.gateway_id) {
data2.gateway = {id: data.gateway_id};
}
return data2;
},
async searchGatewaysToOpen({limit, searchTerm, gateway_id}) {
const domain = [
["channel_type", "=", "gateway"],
["name", "ilike", searchTerm],
["gateway_id", "=", gateway_id],
];
const fields = ["channel_type", "name"];
const channelsData = await this.messaging.rpc({
model: "mail.channel",
method: "search_read",
kwargs: {
domain,
fields,
limit,
},
});
return this.insert(
channelsData.map((channelData) =>
this.messaging.models.Thread.convertData({
channel: {
channel_type: channelData.channel_type,
id: channelData.id,
},
id: channelData.id,
name: channelData.name,
})
)
);
},
},
});

View File

@ -1,9 +0,0 @@
.o_thread_message_broker_received {
color: theme-color("info");
}
.o_thread_message_broker_sent {
color: theme-color("success");
}
.o_thread_message_broker_error {
color: theme-color("danger");
}

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="mail_broker.broker">
<div class="o_widget_Discuss" />
</t>
<t t-name="mail_broker.DiscussSidebarBroker" owl="1">
<div>
<div class="o_DiscussSidebar_groupHeader">
<div
class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle o-clickable"
t-esc="mailBroker.name"
/>
<div class="o-autogrow" />
<div
class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupHeaderItemAdd fa fa-plus"
t-on-click="_onClickChannelAdd"
title="Add or join a channel"
/>
</div>
<div class="o_DiscussSidebar_list">
<div class="o_DiscussSidebar_item o_DiscussSidebar_itemNew">
<AutocompleteInput
class="o_DiscussSidebar_itemNewInput"
customClass="'o_DiscussSidebar_newChannelAutocompleteSuggestions'"
isFocusOnMount="true"
isHtml="true"
placeholder="FIND_OR_CREATE_CHANNEL"
select="_onAddChannelAutocompleteSelect"
source="_onAddChannelAutocompleteSource"
/>
</div>
<t
t-foreach="quickSearchOrderedAndPinnedBrokerChannels"
t-as="brokerChannel"
t-key="brokerChannel.localId"
>
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="brokerChannel.localId"
/>
</t>
</div>
</div>
</t>
<t t-name="mail_broker.DiscussSidebar" owl="1">
<div name="root" class="o_DiscussSidebar">
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupChannel">
<div class="o_DiscussSidebar_list">
<t
t-foreach="mailBrokers"
t-as="mailBroker"
t-key="mailBroker.localId"
>
<DiscussSidebarBroker brokerId="mailBroker.localId" />
</t>
</div>
</div>
</div>
</t>
<!--
<t t-extend="mail.widget.Thread.Message">
<t t-jquery=".o_mail_info" t-operation="append">
<span
t-if="message.broker_type"
class="o_thread_tooltip_broker_container"
/>
</t>
</t>
<t t-name="mail_broker.broker">
<div class="o_mail_discuss">
<div class="o_mail_discuss_sidebar" />
<div class="o_mail_discuss_content">
</div>
</div>
</t>
<t t-name="mail_broker.broker.Sidebar">
<div class="o_mail_discuss_sidebar">
<t t-foreach="bots" t-as="bot_key">
<t t-set="type">broker_thread</t>
<t t-set="bot" t-value="bots[bot_key]" />
<t t-set="channels" t-value="bot.threads" />
<t t-call="mail.broker.discuss.SidebarTitle">
<t t-set="title" t-value="bot.name" />
<t t-set="icon" t-value="fa-users" />
</t>
<t t-call="mail.broker.discuss.SidebarItems">
<t t-set="displayHash" t-value="false" />
<t t-set="inputPlaceholder">Search a chat</t>
</t>
</t>
</div>
</t>
<t t-name="mail.broker.discuss.SidebarTitle" t-extend="mail.discuss.SidebarTitle">
<t t-jquery=".o_add" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
</t>
<t t-name="mail.broker.discuss.SidebarItems" t-extend="mail.discuss.SidebarItems">
<t t-jquery=".o_mail_add_thread" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
<t t-jquery=".o_input" t-operation="attributes">
<attribute name="t-attf-data-bot">#{bot_key}</attribute>
</t>
</t>-->
</templates>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="assets_backend" name="Broker assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/mail_broker/static/src/js/message.js"
/>
<script
type="text/javascript"
src="/mail_broker/static/src/js/discuss.js"
/><!--
<script
type="text/javascript"
src="/mail_broker/static/src/js/messaging_initializer.js"
/>-->
<script
type="text/javascript"
src="/mail_broker/static/src/js/broker_model.js"
/>
<script type="text/javascript" src="/mail_broker/static/src/js/broker.js" />
<link rel="stylesheet" href="/mail_broker/static/src/scss/broker.scss" />
</xpath>
</template>
</odoo>

View File

@ -1,15 +1,14 @@
# Copyright 2022 CreuBlanca
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.addons.component.tests.common import SavepointComponentRegistryCase
from odoo.tests.common import HttpCase
class MailBrokerComponentRegistryTestCase(SavepointComponentRegistryCase):
class MailGatewayTestCase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_env()
cls._load_module_components(cls, "mail_broker")
@classmethod
def _setup_context(cls):

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="mail_broker_action_window" model="ir.actions.client">
<field name="name">Broker</field>
<field name="tag">mail.broker</field>
<field name="res_model">mail.channel</field>
<field name="params" eval="{}" />
</record>
<menuitem
name="Broker"
id="mail_broker_channel"
sequence="2"
web_icon="mail_broker,static/description/icon.png"
action="mail_broker_action_window"
groups="mail_broker.broker_user"
/>
</odoo>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca
<!-- Copyright 2024 Dixmit
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_broker_form_view">
<field name="name">mail.broker.form</field>
<field name="model">mail.broker</field>
<record model="ir.ui.view" id="mail_gateway_form_view">
<field name="name">mail.gateway.form</field>
<field name="model">mail.gateway</field>
<field name="arch" type="xml">
<form>
<header />
@ -35,32 +35,41 @@
<field name="can_set_webhook" invisible="1" />
<group>
<field name="name" />
<field name="show_on_app" />
<field name="token" />
<field name="broker_type" />
<field name="token" password="True" />
<field name="gateway_type" />
<field name="integrated_webhook_state" />
<field name="webhook_url" groups="base.group_no_one" />
<field name="webhook_key" />
<field name="webhook_secret" />
<field name="webhook_secret" password="True" />
<field name="webhook_user_id" />
<field name="has_new_channel_security" />
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page name="member" string="Members">
<field name="member_ids" />
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_search_view">
<field name="name">mail.broker.search</field>
<field name="model">mail.broker</field>
<record model="ir.ui.view" id="mail_gateway_search_view">
<field name="name">mail.gateway.search</field>
<field name="model">mail.gateway</field>
<field name="arch" type="xml">
<search>
<field name="name" />
</search>
</field>
</record>
<record model="ir.ui.view" id="mail_broker_tree_view">
<field name="name">mail.broker.tree</field>
<field name="model">mail.broker</field>
<record model="ir.ui.view" id="mail_gateway_tree_view">
<field name="name">mail.gateway.tree</field>
<field name="model">mail.gateway</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
@ -68,17 +77,17 @@
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="mail_broker_act_window">
<field name="name">Mail Broker</field>
<field name="res_model">mail.broker</field>
<record model="ir.actions.act_window" id="mail_gateway_act_window">
<field name="name">Gateway</field>
<field name="res_model">mail.gateway</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="mail_broker_menu">
<field name="name">Mail Broker</field>
<record model="ir.ui.menu" id="mail_gateway_menu">
<field name="name">Gateway</field>
<field name="parent_id" ref="base.menu_email" />
<field name="action" ref="mail_broker_act_window" />
<field name="action" ref="mail_gateway_act_window" />
<field name="sequence" eval="16" />
</record>
</odoo>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Dixmit
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<odoo>
<record model="ir.ui.view" id="view_res_partner_gateway_channel_form">
<field name="name">Partner Gateway Channel Form</field>
<field name="model">res.partner.gateway.channel</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="partner_id" />
<field name="gateway_id" />
<field name="gateway_token" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_res_partner_gateway_channel_tree">
<field name="name">Partner Gateway Channel Tree</field>
<field name="model">res.partner.gateway.channel</field>
<field name="arch" type="xml">
<tree>
<field name="partner_id" />
<field name="gateway_id" />
<field name="gateway_token" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_res_partner_gateway_channel_search">
<field name="name">Partner Gateway Channel Tree</field>
<field name="model">res.partner.gateway.channel</field>
<field name="arch" type="xml">
<search>
<field name="partner_id" />
<field name="gateway_id" />
<field name="gateway_token" />
</search>
</field>
</record>
<record model="ir.actions.act_window" id="res_partner_gateway_channel_act_window">
<field name="name">Gateway Partner Channels</field>
<field name="res_model">res.partner.gateway.channel</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="res_partner_gateway_channel_menu">
<field name="name">Gateway Partner Channels</field>
<field name="parent_id" ref="base.menu_email" />
<field name="action" ref="res_partner_gateway_channel_act_window" />
<field name="sequence" eval="16" />
</record>
</odoo>

View File

@ -0,0 +1,3 @@
from . import mail_guest_manage
from . import mail_message_gateway_send
from . import mail_message_gateway_link

View File

@ -0,0 +1,58 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MailGuestManage(models.TransientModel):
_name = "mail.guest.manage"
_description = "Assign gateway guest to a partner"
guest_id = fields.Many2one("mail.guest", required=True)
partner_id = fields.Many2one("res.partner")
def create_partner(self):
partner = self.env["res.partner"].create(self._get_partner_vals())
self._merge_partner(partner)
return partner.get_formview_action()
def _get_partner_vals(self):
return {
"name": self.guest_id.name,
}
def _merge_partner(self, partner):
self.env["res.partner.gateway.channel"].create(
{
"name": self.guest_id.gateway_id.name,
"partner_id": partner.id,
"gateway_id": self.guest_id.gateway_id.id,
"gateway_token": self.guest_id.gateway_token,
}
)
for member in self.env["mail.channel.member"].search(
[("guest_id", "=", self.guest_id.id)]
):
self.env["mail.channel.member"].create(
self._channel_member_vals(member, partner)
)
member.unlink()
self.env["mail.message"].search(
[("author_guest_id", "=", self.guest_id.id)]
).write(
{
"author_id": partner.id,
}
)
def _channel_member_vals(self, member, partner):
return {
"guest_id": False,
"channel_id": member.channel_id.id,
"partner_id": partner.id,
}
def merge_partner(self):
self._merge_partner(self.partner_id)
return self.partner_id.get_formview_action()

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Dixmit
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_guest_manage_form_view">
<field name="model">mail.guest.manage</field>
<field name="arch" type="xml">
<form>
<group>
<field name="guest_id" force_save="1" readonly="1" />
<field name="partner_id" />
</group>
<footer>
<button
name="create_partner"
string="Create new partner"
class="btn-primary"
type="object"
attrs="{'invisible': [('partner_id', '!=', False)]}"
/>
<button
name="merge_partner"
string="Merge"
class="btn-primary"
attrs="{'invisible': [('partner_id', '=', False)]}"
type="object"
/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,42 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class MailMessageGatewayLink(models.TransientModel):
_name = "mail.message.gateway.link"
_description = "Link message from gateway"
message_id = fields.Many2one("mail.message")
resource_ref = fields.Reference(
string="Record reference", selection="_selection_target_model"
)
@api.model
def _selection_target_model(self):
models = self.env["ir.model"].search([("is_mail_thread", "=", True)])
return [(model.model, model.name) for model in models]
def link_message(self):
new_message = self.resource_ref.message_post(
body=self.message_id.body,
author_id=self.message_id.author_id.id,
gateway_type=self.message_id.gateway_type,
date=self.message_id.date,
# message_id=update.message.message_id,
subtype_xmlid="mail.mt_comment",
message_type="comment",
attachment_ids=self.message_id.attachment_ids.ids,
gateway_notifications=[], # Avoid sending notifications
)
self.message_id.gateway_message_id = new_message
self.env["bus.bus"]._sendone(
self.env.user.partner_id,
"mail.message/insert",
{
"id": self.message_id.id,
"gateway_thread_data": self.message_id.sudo().gateway_thread_data,
},
)

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Dixmit
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_message_gateway_link_form_view">
<field name="model">mail.message.gateway.link</field>
<field name="arch" type="xml">
<form string="Mail Message Gateway Link">
<!-- TODO -->
<group>
<field name="message_id" invisible="1" />
<field name="resource_ref" />
</group>
<footer>
<button
name="link_message"
string="Link"
class="btn-primary"
type="object"
/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,20 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MailMessageGatewaySend(models.TransientModel):
_name = "mail.message.gateway.send"
_description = "Send Message through gateway"
message_id = fields.Many2one("mail.message", required=True)
partner_id = fields.Many2one("res.partner", required=True)
gateway_channel_id = fields.Many2one(
"res.partner.gateway.channel",
required=True,
)
def send(self):
self.message_id._send_to_gateway_thread(self.gateway_channel_id)

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Dixmit
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="mail_message_gateway_send_form_view">
<field name="model">mail.message.gateway.send</field>
<field name="arch" type="xml">
<form string="Mail Message Gateway Send">
<!-- TODO -->
<group>
<field name="message_id" invisible="1" />
<field
name="partner_id"
domain="[('id', 'in', context.get('partners'))]"
options="{'no_open': 1, 'no_create': 1}"
/>
<field
name="gateway_channel_id"
domain="[('id', 'in', context.get('channels')), ('partner_id', '=', partner_id)]"
options="{'no_open': 1, 'no_create': 1}"
/>
</group>
<footer>
<button
name="send"
string="Send"
class="btn-primary"
type="object"
/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
../../../../mail_gateway

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)