initial commit
This commit is contained in:
commit
6e6f15d803
54
README.md
Normal file
54
README.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
Odoo Enterprise Social Network
|
||||
------------------------------
|
||||
|
||||
Connect with experts, follow what interests you, share documents and promote
|
||||
best practices with Odoo <a href="https://www.odoo.com/app/discuss">Enterprise Social Network</a>. Get work done with
|
||||
effective collaboration across departments, geographies, documents and business
|
||||
applications. All of this while decreasing email overload.
|
||||
|
||||
Connect with experts
|
||||
--------------------
|
||||
|
||||
Next time you have a question for the marketing, sales, R&D or any other
|
||||
department, don't send an email blast-post the question to Odoo and get answers
|
||||
from the right persons.
|
||||
|
||||
Follow what interests you
|
||||
-------------------------
|
||||
|
||||
Want to get informed about new product features, hot deals, bottlenecks in
|
||||
projects or any other event? Just follow what interests you to get the
|
||||
information you need what you need; no more, no less.
|
||||
|
||||
Get Things Done
|
||||
---------------
|
||||
|
||||
You can process (not only read) the inbox and easily mark messages for future
|
||||
actions. Start feeling the pleasure of having an empty inbox every day; no more
|
||||
overload of information.
|
||||
|
||||
Promote best practices
|
||||
----------------------
|
||||
|
||||
Cut back on meetings and email chains by working together in groups of
|
||||
interests. Create a group to let people share files, discuss ideas, and vote to
|
||||
promote best practices.
|
||||
|
||||
Improve Access to Information and Expertise
|
||||
-------------------------------------------
|
||||
|
||||
Break down information silos. Search across your existing systems to find the
|
||||
answers and expertise you need to complete projects quickly.
|
||||
|
||||
Collaborate securely
|
||||
--------------------
|
||||
|
||||
Set the right security policy; public, private or on invitation only --
|
||||
according to the information sensitivity.
|
||||
|
||||
A Twitter-like Network For My Company
|
||||
---------------------------------------
|
||||
|
||||
Make every employee feel more connected and engaged with twitter-like features
|
||||
for your own company. Follow people, share best practices, 'like' top ideas,
|
||||
etc.
|
11
__init__.py
Normal file
11
__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import tools
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
from . import populate
|
||||
|
||||
def _mail_post_init(env):
|
||||
env['mail.alias.domain']._migrate_icp_to_domain()
|
229
__manifest__.py
Normal file
229
__manifest__.py
Normal file
|
@ -0,0 +1,229 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Discuss',
|
||||
'version': '1.15',
|
||||
'category': 'Productivity/Discuss',
|
||||
'sequence': 145,
|
||||
'summary': 'Chat, mail gateway and private channels',
|
||||
'description': """
|
||||
|
||||
Chat, mail gateway and private channel.
|
||||
=======================================
|
||||
|
||||
Communicate with your colleagues/customers/guest within Odoo.
|
||||
|
||||
Discuss/Chat
|
||||
------------
|
||||
User-friendly "Discuss" features that allows one 2 one or group communication
|
||||
(text chat/voice call/video call), invite guests and share documents with
|
||||
them, all real-time.
|
||||
|
||||
Mail gateway
|
||||
------------
|
||||
Sending information and documents made simplified. You can send emails
|
||||
from Odoo itself, and that too with great possibilities. For example,
|
||||
design a beautiful email template for the invoices, and use the same
|
||||
for all your customers, no need to do the same exercise every time.
|
||||
|
||||
Chatter
|
||||
-------
|
||||
Do all the contextual conversation on a document. For example on an
|
||||
applicant, directly post an update to send email to the applicant,
|
||||
schedule the next interview call, attach the contract, add HR officer
|
||||
to the follower list to notify them for important events(with help of
|
||||
subtypes),...
|
||||
|
||||
|
||||
Retrieve incoming email on POP/IMAP servers.
|
||||
============================================
|
||||
Enter the parameters of your POP/IMAP account(s), and any incoming emails on
|
||||
these accounts will be automatically downloaded into your Odoo system. All
|
||||
POP3/IMAP-compatible servers are supported, included those that require an
|
||||
encrypted SSL/TLS connection.
|
||||
This can be used to easily create email-based workflows for many email-enabled Odoo documents, such as:
|
||||
----------------------------------------------------------------------------------------------------------
|
||||
* CRM Leads/Opportunities
|
||||
* CRM Claims
|
||||
* Project Issues
|
||||
* Project Tasks
|
||||
* Human Resource Recruitment (Applicants)
|
||||
Just install the relevant application, and you can assign any of these document
|
||||
types (Leads, Project Issues) to your incoming email accounts. New emails will
|
||||
automatically spawn new documents of the chosen type, so it's a snap to create a
|
||||
mailbox-to-Odoo integration. Even better: these documents directly act as mini
|
||||
conversations synchronized by email. You can reply from within Odoo, and the
|
||||
answers will automatically be collected when they come back, and attached to the
|
||||
same *conversation* document.
|
||||
For more specific needs, you may also assign custom-defined actions
|
||||
(technically: Server Actions) to be triggered for each incoming mail.
|
||||
""",
|
||||
'website': 'https://www.odoo.com/app/discuss',
|
||||
'depends': ['base', 'base_setup', 'bus', 'web_tour'],
|
||||
'data': [
|
||||
'data/mail_groups.xml',
|
||||
'wizard/mail_activity_schedule_views.xml',
|
||||
'wizard/mail_blacklist_remove_views.xml',
|
||||
'wizard/mail_compose_message_views.xml',
|
||||
'wizard/mail_resend_message_views.xml',
|
||||
'wizard/mail_resend_partner_views.xml',
|
||||
'wizard/mail_template_preview_views.xml',
|
||||
'wizard/mail_wizard_invite_views.xml',
|
||||
'wizard/mail_template_reset_views.xml',
|
||||
'views/fetchmail_views.xml',
|
||||
'views/mail_message_subtype_views.xml',
|
||||
'views/mail_tracking_value_views.xml',
|
||||
'views/mail_notification_views.xml',
|
||||
'views/mail_message_views.xml',
|
||||
'views/mail_message_schedule_views.xml',
|
||||
'views/mail_mail_views.xml',
|
||||
'views/mail_followers_views.xml',
|
||||
'views/mail_ice_server_views.xml',
|
||||
'views/discuss_channel_member_views.xml',
|
||||
'views/discuss_channel_rtc_session_views.xml',
|
||||
'views/mail_link_preview_views.xml',
|
||||
'views/discuss/discuss_gif_favorite_views.xml',
|
||||
'views/discuss_channel_views.xml',
|
||||
'views/mail_shortcode_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'views/mail_activity_plan_views.xml',
|
||||
'views/mail_activity_plan_template_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'data/ir_config_parameter_data.xml',
|
||||
'data/res_partner_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_templates_chatter.xml',
|
||||
'data/mail_templates_email_layouts.xml',
|
||||
'data/mail_templates_mailgateway.xml',
|
||||
'data/discuss_channel_data.xml',
|
||||
'data/mail_activity_data.xml',
|
||||
'data/security_notifications_templates.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'security/mail_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/discuss_public_templates.xml',
|
||||
'views/mail_alias_domain_views.xml',
|
||||
'views/mail_alias_views.xml',
|
||||
'views/mail_gateway_allowed_views.xml',
|
||||
'views/mail_guest_views.xml',
|
||||
'views/mail_message_reaction_views.xml',
|
||||
'views/mail_templates_public.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/res_users_settings_views.xml',
|
||||
'views/mail_template_views.xml',
|
||||
'views/ir_actions_server_views.xml',
|
||||
'views/ir_model_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/mail_blacklist_views.xml',
|
||||
'views/mail_menus.xml',
|
||||
'views/res_company_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/discuss_channel_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'post_init_hook': '_mail_post_init',
|
||||
'assets': {
|
||||
'web._assets_primary_variables': [
|
||||
'mail/static/src/**/primary_variables.scss',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
# depends on BS variables, can't be loaded in assets_primary or assets_secondary
|
||||
'mail/static/src/scss/variables/derived_variables.scss',
|
||||
'mail/static/src/scss/*.scss',
|
||||
'mail/static/lib/**/*',
|
||||
('remove', 'mail/static/lib/odoo_sfu/odoo_sfu.js'),
|
||||
('remove', 'mail/static/lib/lame/lame.js'),
|
||||
'mail/static/src/js/**/*',
|
||||
'mail/static/src/core/common/**/*',
|
||||
'mail/static/src/core/web_portal/**/*',
|
||||
'mail/static/src/core/web/**/*',
|
||||
'mail/static/src/**/common/**/*',
|
||||
'mail/static/src/**/web/**/*',
|
||||
('remove', 'mail/static/src/core/web/wysiwyg.js'),
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
# discuss (loaded last to fix dependencies)
|
||||
('remove', 'mail/static/src/discuss/**/*'),
|
||||
'mail/static/src/discuss/core/common/**/*',
|
||||
'mail/static/src/discuss/core/public_web/**/*',
|
||||
'mail/static/src/discuss/core/web/**/*',
|
||||
'mail/static/src/discuss/**/common/**/*',
|
||||
'mail/static/src/discuss/**/public_web/**/*',
|
||||
'mail/static/src/discuss/**/web/**/*',
|
||||
('remove', 'mail/static/src/discuss/**/*.dark.scss'),
|
||||
'mail/static/src/views/fields/**/*',
|
||||
],
|
||||
'web_editor.backend_assets_wysiwyg': [
|
||||
'mail/static/src/core/web/wysiwyg.js',
|
||||
],
|
||||
"web.assets_web_dark": [
|
||||
'mail/static/src/**/*.dark.scss',
|
||||
],
|
||||
'mail.assets_discuss_public_test_tours': [
|
||||
'web_tour/static/src/tour_pointer/**/*',
|
||||
# scss not needed in tests and depends on scss variables that are not in this bundle
|
||||
('remove', 'web_tour/static/src/tour_pointer/**/*.scss'),
|
||||
'web_tour/static/src/tour_service/**/*',
|
||||
'web/static/tests/helpers/cleanup.js',
|
||||
'web/static/tests/helpers/utils.js',
|
||||
'web/static/tests/utils.js',
|
||||
'mail/static/tests/tours/discuss_channel_public_tour.js',
|
||||
'mail/static/tests/tours/discuss_channel_as_guest_tour.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'mail/static/tests/tours/**/*',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'mail/static/tests/helpers/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'mail/static/tests/**/*',
|
||||
('remove', 'mail/static/tests/tours/**/*'),
|
||||
('remove', 'mail/static/tests/helpers/**/*'),
|
||||
],
|
||||
'mail.assets_odoo_sfu': [
|
||||
'mail/static/lib/odoo_sfu/odoo_sfu.js',
|
||||
],
|
||||
'mail.assets_lamejs': [
|
||||
'mail/static/lib/lame/lame.js',
|
||||
],
|
||||
'mail.assets_public': [
|
||||
'web/static/lib/jquery/jquery.js',
|
||||
'web/static/lib/odoo_ui_icons/style.css',
|
||||
'web/static/src/libs/fontawesome/css/font-awesome.css',
|
||||
('include', 'web._assets_helpers'),
|
||||
('include', 'web._assets_backend_helpers'),
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
('include', 'web._assets_bootstrap_backend'),
|
||||
'web/static/src/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/webclient/webclient.scss',
|
||||
'web/static/src/scss/mimetypes.scss',
|
||||
('include', 'web._assets_core'),
|
||||
'web/static/src/libs/pdfjs.js',
|
||||
'web/static/src/views/fields/formatters.js',
|
||||
'web/static/src/views/fields/file_handler.*',
|
||||
|
||||
'bus/static/src/*.js',
|
||||
'bus/static/src/services/**/*.js',
|
||||
'bus/static/src/workers/websocket_worker.js',
|
||||
'bus/static/src/workers/websocket_worker_utils.js',
|
||||
|
||||
'mail/static/src/core/common/**/*',
|
||||
'mail/static/src/**/common/**/*',
|
||||
'mail/static/src/**/public/**/*',
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
# discuss (loaded last to fix dependencies)
|
||||
('remove', 'mail/static/src/discuss/**/*'),
|
||||
'mail/static/src/discuss/core/common/**/*',
|
||||
'mail/static/src/discuss/core/public_web/**/*',
|
||||
'mail/static/src/discuss/core/public/**/*',
|
||||
'mail/static/src/discuss/**/common/**/*',
|
||||
'mail/static/src/discuss/**/public/**/*',
|
||||
'mail/static/src/discuss/**/public_web/**/*',
|
||||
('remove', 'mail/static/src/discuss/**/*.dark.scss'),
|
||||
]
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/web_push.cpython-311.pyc
Normal file
BIN
__pycache__/web_push.cpython-311.pyc
Normal file
Binary file not shown.
15
controllers/__init__.py
Normal file
15
controllers/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import attachment
|
||||
from . import google_translate
|
||||
from . import guest
|
||||
from . import link_preview
|
||||
from . import mail
|
||||
from . import mailbox
|
||||
from . import message_reaction
|
||||
from . import thread
|
||||
from . import webclient
|
||||
from . import webmanifest
|
||||
|
||||
# after mail specifically as discuss module depends on mail
|
||||
from . import discuss
|
BIN
controllers/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/attachment.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/attachment.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/google_translate.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/google_translate.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/guest.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/guest.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/link_preview.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/link_preview.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/mail.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/mail.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/mailbox.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/mailbox.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/message_reaction.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/message_reaction.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/thread.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/thread.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/webclient.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/webclient.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/__pycache__/webmanifest.cpython-311.pyc
Normal file
BIN
controllers/__pycache__/webmanifest.cpython-311.pyc
Normal file
Binary file not shown.
124
controllers/attachment.py
Normal file
124
controllers/attachment.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import io
|
||||
import logging
|
||||
import zipfile
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request, content_disposition
|
||||
|
||||
from odoo.tools import consteq
|
||||
from ..models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AttachmentController(http.Controller):
|
||||
def _make_zip(self, name, attachments):
|
||||
streams = (request.env['ir.binary']._get_stream_from(record, 'raw') for record in attachments)
|
||||
# TODO: zip on-the-fly while streaming instead of loading the
|
||||
# entire zip in memory and sending it all at once.
|
||||
stream = io.BytesIO()
|
||||
try:
|
||||
with zipfile.ZipFile(stream, 'w') as attachment_zip:
|
||||
for binary_stream in streams:
|
||||
if not binary_stream:
|
||||
continue
|
||||
attachment_zip.writestr(
|
||||
binary_stream.download_name,
|
||||
binary_stream.read(),
|
||||
compress_type=zipfile.ZIP_DEFLATED
|
||||
)
|
||||
except zipfile.BadZipFile:
|
||||
logger.exception("BadZipfile exception")
|
||||
|
||||
content = stream.getvalue()
|
||||
headers = [
|
||||
('Content-Type', 'zip'),
|
||||
('X-Content-Type-Options', 'nosniff'),
|
||||
('Content-Length', len(content)),
|
||||
('Content-Disposition', content_disposition(name))
|
||||
]
|
||||
return request.make_response(content, headers)
|
||||
|
||||
@http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
||||
thread = request.env[thread_model].with_context(active_test=False).search([("id", "=", thread_id)])
|
||||
if not thread:
|
||||
raise NotFound()
|
||||
if thread_model == "discuss.channel" and not thread.allow_public_upload and not request.env.user._is_internal():
|
||||
raise AccessError(_("You are not allowed to upload attachments on this channel."))
|
||||
vals = {
|
||||
"name": ufile.filename,
|
||||
"raw": ufile.read(),
|
||||
"res_id": int(thread_id),
|
||||
"res_model": thread_model,
|
||||
}
|
||||
if is_pending and is_pending != "false":
|
||||
# Add this point, the message related to the uploaded file does
|
||||
# not exist yet, so we use those placeholder values instead.
|
||||
vals.update(
|
||||
{
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
}
|
||||
)
|
||||
if request.env.user.share:
|
||||
# Only generate the access token if absolutely necessary (= not for internal user).
|
||||
vals["access_token"] = request.env["ir.attachment"]._generate_access_token()
|
||||
try:
|
||||
# sudo: ir.attachment - posting a new attachment on an accessible thread
|
||||
attachment = request.env["ir.attachment"].sudo().create(vals)
|
||||
attachment._post_add_create(**kwargs)
|
||||
attachmentData = attachment._attachment_format()[0]
|
||||
if attachment.access_token:
|
||||
attachmentData["accessToken"] = attachment.access_token
|
||||
except AccessError:
|
||||
attachmentData = {"error": _("You are not allowed to upload an attachment here.")}
|
||||
return request.make_json_response(attachmentData)
|
||||
|
||||
@http.route("/mail/attachment/delete", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_attachment_delete(self, attachment_id, access_token=None):
|
||||
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
|
||||
if not attachment:
|
||||
target = request.env.user.partner_id
|
||||
request.env["bus.bus"]._sendone(target, "ir.attachment/delete", {"id": attachment_id})
|
||||
return
|
||||
message = request.env["mail.message"].search([("attachment_ids", "in", attachment.ids)], limit=1)
|
||||
if not request.env.user.share:
|
||||
# Check through standard access rights/rules for internal users.
|
||||
attachment._delete_and_notify(message)
|
||||
return
|
||||
# For non-internal users 2 cases are supported:
|
||||
# - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest).
|
||||
# - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments).
|
||||
# sudo: ir.attachment: access is validated below with membership of message or access token
|
||||
attachment_sudo = attachment.sudo()
|
||||
if message:
|
||||
if not message.is_current_user_or_guest_author:
|
||||
raise NotFound()
|
||||
else:
|
||||
if (
|
||||
not access_token
|
||||
or not attachment_sudo.access_token
|
||||
or not consteq(access_token, attachment_sudo.access_token)
|
||||
):
|
||||
raise NotFound()
|
||||
if attachment_sudo.res_model != "mail.compose.message" or attachment_sudo.res_id != 0:
|
||||
raise NotFound()
|
||||
attachment_sudo._delete_and_notify(message)
|
||||
|
||||
@http.route(['/mail/attachment/zip'], methods=["POST"], type="http", auth="public")
|
||||
def mail_attachment_get_zip(self, file_ids, zip_name, **kw):
|
||||
"""route to get the zip file of the attachments.
|
||||
:param file_ids: ids of the files to zip.
|
||||
:param zip_name: name of the zip file.
|
||||
"""
|
||||
ids_list = list(map(int, file_ids.split(',')))
|
||||
attachments = request.env['ir.attachment'].browse(ids_list)
|
||||
return self._make_zip(zip_name, attachments)
|
8
controllers/discuss/__init__.py
Normal file
8
controllers/discuss/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import binary
|
||||
from . import channel
|
||||
from . import gif
|
||||
from . import public_page
|
||||
from . import rtc
|
||||
from . import voice
|
BIN
controllers/discuss/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/binary.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/binary.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/channel.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/channel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/gif.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/gif.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/public_page.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/public_page.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/rtc.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/rtc.cpython-311.pyc
Normal file
Binary file not shown.
BIN
controllers/discuss/__pycache__/voice.cpython-311.pyc
Normal file
BIN
controllers/discuss/__pycache__/voice.cpython-311.pyc
Normal file
Binary file not shown.
94
controllers/discuss/binary.py
Normal file
94
controllers/discuss/binary.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
from odoo.addons.web.controllers.binary import Binary
|
||||
|
||||
|
||||
class BinaryController(Binary):
|
||||
@http.route(
|
||||
"/discuss/channel/<int:channel_id>/partner/<int:partner_id>/avatar_128",
|
||||
methods=["GET"],
|
||||
type="http",
|
||||
auth="public",
|
||||
)
|
||||
@add_guest_to_context
|
||||
def discuss_channel_partner_avatar_128(self, channel_id, partner_id, unique=False):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
partner = request.env["res.partner"].browse(partner_id).exists()
|
||||
domain = [("channel_id", "=", channel_id), ("partner_id", "=", partner_id)]
|
||||
if channel and partner and request.env["discuss.channel.member"].search(domain):
|
||||
# sudo: res.partner - the partner is in the same channel as the current user, so they can see their avatar
|
||||
return (
|
||||
request.env["ir.binary"]._get_image_stream_from(partner.sudo(), field_name="avatar_128").get_response(immutable=True if unique else False)
|
||||
)
|
||||
return self.content_image(model="res.partner", id=partner_id, field="avatar_128", unique=unique)
|
||||
|
||||
@http.route(
|
||||
"/discuss/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128", methods=["GET"], type="http", auth="public"
|
||||
)
|
||||
@add_guest_to_context
|
||||
def discuss_channel_guest_avatar_128(self, channel_id, guest_id, unique=False):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
guest = request.env["mail.guest"].browse(guest_id).exists()
|
||||
domain = [("channel_id", "=", channel_id), ("guest_id", "=", guest_id)]
|
||||
if channel and guest and request.env["discuss.channel.member"].search(domain):
|
||||
# sudo: mail.guest - the guest is in the same channel as the current user, so they can see their avatar
|
||||
return request.env["ir.binary"]._get_image_stream_from(guest.sudo(), field_name="avatar_128").get_response(immutable=True if unique else False)
|
||||
return self.content_image(model="mail.guest", id=guest_id, field="avatar_128", unique=unique)
|
||||
|
||||
@http.route(
|
||||
"/discuss/channel/<int:channel_id>/attachment/<int:attachment_id>", methods=["GET"], type="http", auth="public"
|
||||
)
|
||||
@add_guest_to_context
|
||||
def discuss_channel_attachment(self, channel_id, attachment_id, download=None, **kwargs):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
domain = [
|
||||
("id", "=", int(attachment_id)),
|
||||
("res_id", "=", int(channel_id)),
|
||||
("res_model", "=", "discuss.channel"),
|
||||
]
|
||||
# sudo: ir.attachment - searching for an attachment on a specific channel that the current user can access
|
||||
attachment_sudo = request.env["ir.attachment"].sudo().search(domain)
|
||||
if not attachment_sudo:
|
||||
raise NotFound()
|
||||
return request.env["ir.binary"]._get_stream_from(attachment_sudo).get_response(as_attachment=download)
|
||||
|
||||
@http.route("/discuss/channel/<int:channel_id>/avatar_128", methods=["GET"], type="http", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_avatar_128(self, channel_id, unique=False):
|
||||
return self.content_image(model="discuss.channel", id=channel_id, field="avatar_128", unique=unique)
|
||||
|
||||
@http.route(
|
||||
[
|
||||
"/discuss/channel/<int:channel_id>/image/<int:attachment_id>",
|
||||
"/discuss/channel/<int:channel_id>/image/<int:attachment_id>/<int:width>x<int:height>",
|
||||
],
|
||||
methods=["GET"],
|
||||
type="http",
|
||||
auth="public",
|
||||
)
|
||||
@add_guest_to_context
|
||||
def fetch_image(self, channel_id, attachment_id, width=0, height=0, **kwargs):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
domain = [
|
||||
("id", "=", attachment_id),
|
||||
("res_id", "=", channel_id),
|
||||
("res_model", "=", "discuss.channel"),
|
||||
]
|
||||
# sudo: ir.attachment - searching for an attachment on a specific channel that the current user can access
|
||||
attachment_sudo = request.env["ir.attachment"].sudo().search(domain, limit=1)
|
||||
if not attachment_sudo:
|
||||
raise NotFound()
|
||||
return (
|
||||
request.env["ir.binary"]
|
||||
._get_image_stream_from(attachment_sudo, width=int(width), height=int(height))
|
||||
.get_response(as_attachment=kwargs.get("download"))
|
||||
)
|
141
controllers/discuss/channel.py
Normal file
141
controllers/discuss/channel.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class ChannelController(http.Controller):
|
||||
@http.route("/discuss/channel/members", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_members(self, channel_id, known_member_ids):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
return channel.load_more_members(known_member_ids)
|
||||
|
||||
@http.route("/discuss/channel/update_avatar", methods=["POST"], type="json")
|
||||
def discuss_channel_avatar_update(self, channel_id, data):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel or not data:
|
||||
raise NotFound()
|
||||
channel.write({"image_128": data})
|
||||
|
||||
@http.route("/discuss/channel/info", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_info(self, channel_id):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
return
|
||||
return channel._channel_info()[0]
|
||||
|
||||
@http.route("/discuss/channel/messages", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_messages(self, channel_id, search_term=None, before=None, after=None, limit=30, around=None):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
domain = [
|
||||
("res_id", "=", channel_id),
|
||||
("model", "=", "discuss.channel"),
|
||||
("message_type", "!=", "user_notification"),
|
||||
]
|
||||
res = request.env["mail.message"]._message_fetch(
|
||||
domain, search_term=search_term, before=before, after=after, around=around, limit=limit
|
||||
)
|
||||
if not request.env.user._is_public() and not around:
|
||||
res["messages"].set_message_done()
|
||||
return {**res, "messages": res["messages"].message_format()}
|
||||
|
||||
@http.route("/discuss/channel/pinned_messages", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_pins(self, channel_id):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
return channel.pinned_message_ids.sorted(key="pinned_at", reverse=True).message_format()
|
||||
|
||||
@http.route("/discuss/channel/mute", methods=["POST"], type="json", auth="user")
|
||||
def discuss_channel_mute(self, channel_id, minutes):
|
||||
"""Mute notifications for the given number of minutes.
|
||||
:param minutes: (integer) number of minutes to mute notifications, -1 means mute until the user unmutes
|
||||
"""
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise request.not_found()
|
||||
if minutes == -1:
|
||||
member.mute_until_dt = datetime.max
|
||||
elif minutes:
|
||||
member.mute_until_dt = fields.Datetime.now() + relativedelta(minutes=minutes)
|
||||
request.env.ref("mail.ir_cron_discuss_channel_member_unmute")._trigger(member.mute_until_dt)
|
||||
else:
|
||||
member.mute_until_dt = False
|
||||
channel_data = {
|
||||
"id": member.channel_id.id,
|
||||
"model": "discuss.channel",
|
||||
"mute_until_dt": member.mute_until_dt,
|
||||
}
|
||||
request.env["bus.bus"]._sendone(member.partner_id, "mail.record/insert", {"Thread": channel_data})
|
||||
|
||||
@http.route("/discuss/channel/update_custom_notifications", methods=["POST"], type="json", auth="user")
|
||||
def discuss_channel_update_custom_notifications(self, channel_id, custom_notifications):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise request.not_found()
|
||||
member.custom_notifications = custom_notifications
|
||||
channel_data = {
|
||||
"custom_notifications": member.custom_notifications,
|
||||
"id": member.channel_id.id,
|
||||
"model": "discuss.channel",
|
||||
}
|
||||
request.env["bus.bus"]._sendone(member.partner_id, "mail.record/insert", {"Thread": channel_data})
|
||||
|
||||
@http.route("/discuss/channel/set_last_seen_message", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_mark_as_seen(self, channel_id, last_message_id, allow_older=False):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
return channel._channel_seen(last_message_id, allow_older=allow_older)
|
||||
|
||||
@http.route("/discuss/channel/notify_typing", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_notify_typing(self, channel_id, is_typing):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise NotFound()
|
||||
member._notify_typing(is_typing)
|
||||
|
||||
@http.route("/discuss/channel/attachments", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def load_attachments(self, channel_id, limit=30, before=None):
|
||||
"""Load attachments of a channel. If before is set, load attachments
|
||||
older than the given id.
|
||||
:param channel_id: id of the channel
|
||||
:param limit: maximum number of attachments to return
|
||||
:param before: id of the attachment from which to load older attachments
|
||||
"""
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
domain = [
|
||||
["res_id", "=", channel_id],
|
||||
["res_model", "=", "discuss.channel"],
|
||||
]
|
||||
if before:
|
||||
domain.append(["id", "<", before])
|
||||
# sudo: ir.attachment - reading attachments of a channel that the current user can access
|
||||
return request.env["ir.attachment"].sudo().search(domain, limit=limit, order="id DESC")._attachment_format()
|
89
controllers/discuss/gif.py
Normal file
89
controllers/discuss/gif.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import requests
|
||||
import werkzeug.urls
|
||||
|
||||
from odoo.http import request, route, Controller
|
||||
|
||||
|
||||
class DiscussGifController(Controller):
|
||||
def _request_gifs(self, endpoint):
|
||||
response = requests.get(
|
||||
f"https://tenor.googleapis.com/v2/{endpoint}", timeout=3
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
@route("/discuss/gif/search", type="json", auth="user")
|
||||
def search(self, search_term, locale="en", country="US", position=None):
|
||||
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
|
||||
ir_config = request.env["ir.config_parameter"].sudo()
|
||||
query_string = werkzeug.urls.url_encode(
|
||||
{
|
||||
"q": search_term,
|
||||
"key": ir_config.get_param("discuss.tenor_api_key"),
|
||||
"client_key": request.env.cr.dbname,
|
||||
"limit": ir_config.get_param("discuss.tenor_gif_limit"),
|
||||
"contentfilter": ir_config.get_param("discuss.tenor_content_filter"),
|
||||
"locale": locale,
|
||||
"country": country,
|
||||
"media_filter": "tinygif",
|
||||
"pos": position,
|
||||
}
|
||||
)
|
||||
response = self._request_gifs(f"search?{query_string}")
|
||||
if response:
|
||||
return response.json()
|
||||
|
||||
@route("/discuss/gif/categories", type="json", auth="user")
|
||||
def categories(self, locale="en", country="US"):
|
||||
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
|
||||
ir_config = request.env["ir.config_parameter"].sudo()
|
||||
query_string = werkzeug.urls.url_encode(
|
||||
{
|
||||
"key": ir_config.get_param("discuss.tenor_api_key"),
|
||||
"client_key": request.env.cr.dbname,
|
||||
"limit": ir_config.get_param("discuss.tenor_gif_limit"),
|
||||
"contentfilter": ir_config.get_param("discuss.tenor_content_filter"),
|
||||
"locale": locale,
|
||||
"country": country,
|
||||
}
|
||||
)
|
||||
response = self._request_gifs(f"categories?{query_string}")
|
||||
if response:
|
||||
return response.json()
|
||||
|
||||
@route("/discuss/gif/add_favorite", type="json", auth="user")
|
||||
def add_favorite(self, tenor_gif_id):
|
||||
request.env["discuss.gif.favorite"].create({"tenor_gif_id": tenor_gif_id})
|
||||
|
||||
def _gif_posts(self, ids):
|
||||
# sudo: ir.config_parameter - read keys are hard-coded and values are only used for server requests
|
||||
ir_config = request.env["ir.config_parameter"].sudo()
|
||||
query_string = werkzeug.urls.url_encode(
|
||||
{
|
||||
"ids": ",".join(ids),
|
||||
"key": ir_config.get_param("discuss.tenor_api_key"),
|
||||
"client_key": request.env.cr.dbname,
|
||||
"media_filter": "tinygif",
|
||||
}
|
||||
)
|
||||
response = self._request_gifs(f"posts?{query_string}")
|
||||
if response:
|
||||
return response.json()["results"]
|
||||
|
||||
@route("/discuss/gif/favorites", type="json", auth="user")
|
||||
def get_favorites(self, offset=0):
|
||||
tenor_gif_ids = request.env["discuss.gif.favorite"].search(
|
||||
[("create_uid", "=", request.env.user.id)], limit=20, offset=offset
|
||||
)
|
||||
return (self._gif_posts(tenor_gif_ids.mapped("tenor_gif_id")) or [],)
|
||||
|
||||
@route("/discuss/gif/remove_favorite", type="json", auth="user")
|
||||
def remove_favorite(self, tenor_gif_id):
|
||||
request.env["discuss.gif.favorite"].search(
|
||||
[
|
||||
("create_uid", "=", request.env.user.id),
|
||||
("tenor_gif_id", "=", tenor_gif_id),
|
||||
]
|
||||
).unlink()
|
126
controllers/discuss/public_page.py
Normal file
126
controllers/discuss/public_page.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
from psycopg2.errorcodes import UNIQUE_VIOLATION
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request
|
||||
from odoo.tools import consteq, replace_exceptions
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class PublicPageController(http.Controller):
|
||||
@http.route(
|
||||
[
|
||||
"/chat/<string:create_token>",
|
||||
"/chat/<string:create_token>/<string:channel_name>",
|
||||
],
|
||||
methods=["GET"],
|
||||
type="http",
|
||||
auth="public",
|
||||
)
|
||||
@add_guest_to_context
|
||||
def discuss_channel_chat_from_token(self, create_token, channel_name=None):
|
||||
return self._response_discuss_channel_from_token(create_token=create_token, channel_name=channel_name)
|
||||
|
||||
@http.route(
|
||||
[
|
||||
"/meet/<string:create_token>",
|
||||
"/meet/<string:create_token>/<string:channel_name>",
|
||||
],
|
||||
methods=["GET"],
|
||||
type="http",
|
||||
auth="public",
|
||||
)
|
||||
@add_guest_to_context
|
||||
def discuss_channel_meet_from_token(self, create_token, channel_name=None):
|
||||
return self._response_discuss_channel_from_token(
|
||||
create_token=create_token, channel_name=channel_name, default_display_mode="video_full_screen"
|
||||
)
|
||||
|
||||
@http.route("/chat/<int:channel_id>/<string:invitation_token>", methods=["GET"], type="http", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_invitation(self, channel_id, invitation_token):
|
||||
channel = request.env["discuss.channel"].browse(channel_id).exists()
|
||||
# sudo: discuss.channel - channel access is validated with invitation_token
|
||||
if not channel or not channel.sudo().uuid or not consteq(channel.sudo().uuid, invitation_token):
|
||||
raise NotFound()
|
||||
return self._response_discuss_channel_invitation(channel)
|
||||
|
||||
@http.route("/discuss/channel/<int:channel_id>", methods=["GET"], type="http", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel(self, channel_id):
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
return self._response_discuss_public_template(channel)
|
||||
|
||||
def _response_discuss_channel_from_token(self, create_token, channel_name=None, default_display_mode=False):
|
||||
# sudo: ir.config_parameter - reading hard-coded key and using it in a simple condition
|
||||
if not request.env["ir.config_parameter"].sudo().get_param("mail.chat_from_token"):
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel - channel access is validated with invitation_token
|
||||
channel_sudo = request.env["discuss.channel"].sudo().search([("uuid", "=", create_token)])
|
||||
if not channel_sudo:
|
||||
try:
|
||||
channel_sudo = channel_sudo.create(
|
||||
{
|
||||
"channel_type": "channel",
|
||||
"default_display_mode": default_display_mode,
|
||||
"group_public_id": None,
|
||||
"name": channel_name or create_token,
|
||||
"uuid": create_token,
|
||||
}
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if e.pgcode != UNIQUE_VIOLATION:
|
||||
raise
|
||||
# concurrent insert attempt: another request created the channel.
|
||||
# commit the current transaction and get the channel.
|
||||
request.env.cr.commit()
|
||||
channel_sudo = channel_sudo.search([("uuid", "=", create_token)])
|
||||
return self._response_discuss_channel_invitation(channel_sudo.sudo(False), is_channel_token_secret=False)
|
||||
|
||||
def _response_discuss_channel_invitation(self, channel, is_channel_token_secret=True):
|
||||
# group restriction takes precedence over token
|
||||
if channel.group_public_id and channel.group_public_id not in request.env.user.groups_id:
|
||||
raise request.not_found()
|
||||
discuss_public_view_data = {
|
||||
"isChannelTokenSecret": is_channel_token_secret,
|
||||
}
|
||||
guest_already_known = channel.env["mail.guest"]._get_guest_from_context()
|
||||
with replace_exceptions(UserError, by=NotFound()):
|
||||
# sudo: mail.guest - creating a guest and its member inside a channel of which they have the token
|
||||
__, guest = channel.sudo()._find_or_create_persona_for_channel(
|
||||
guest_name=_("Guest"),
|
||||
country_code=request.geoip.country_code,
|
||||
timezone=request.env["mail.guest"]._get_timezone_from_request(request),
|
||||
)
|
||||
if guest and not guest_already_known:
|
||||
discuss_public_view_data.update(
|
||||
{
|
||||
"shouldDisplayWelcomeViewInitially": True,
|
||||
}
|
||||
)
|
||||
channel = channel.with_context(guest=guest)
|
||||
return self._response_discuss_public_template(channel, discuss_public_view_data=discuss_public_view_data)
|
||||
|
||||
def _response_discuss_public_template(self, channel, discuss_public_view_data=None):
|
||||
discuss_public_view_data = discuss_public_view_data or {}
|
||||
return request.render(
|
||||
"mail.discuss_public_channel_template",
|
||||
{
|
||||
"data": {
|
||||
"channelData": channel._channel_info()[0],
|
||||
"discussPublicViewData": dict(
|
||||
{
|
||||
"shouldDisplayWelcomeViewInitially": channel.default_display_mode == "video_full_screen",
|
||||
},
|
||||
**discuss_public_view_data,
|
||||
),
|
||||
},
|
||||
"session_info": channel.env["ir.http"].session_info(),
|
||||
},
|
||||
)
|
138
controllers/discuss/rtc.py
Normal file
138
controllers/discuss/rtc.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class RtcController(http.Controller):
|
||||
@http.route("/mail/rtc/session/notify_call_members", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def session_call_notify(self, peer_notifications):
|
||||
"""Sends content to other session of the same channel, only works if the user is the user of that session.
|
||||
This is used to send peer to peer information between sessions.
|
||||
|
||||
:param peer_notifications: list of tuple with the following elements:
|
||||
- int sender_session_id: id of the session from which the content is sent
|
||||
- list target_session_ids: list of the ids of the sessions that should receive the content
|
||||
- string content: the content to send to the other sessions
|
||||
"""
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
notifications_by_session = defaultdict(list)
|
||||
for sender_session_id, target_session_ids, content in peer_notifications:
|
||||
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
|
||||
session_sudo = request.env["discuss.channel.rtc.session"].sudo().browse(int(sender_session_id)).exists()
|
||||
if (
|
||||
not session_sudo
|
||||
or (session_sudo.guest_id and session_sudo.guest_id != guest)
|
||||
or (session_sudo.partner_id and session_sudo.partner_id != request.env.user.partner_id)
|
||||
):
|
||||
continue
|
||||
notifications_by_session[session_sudo].append(([int(sid) for sid in target_session_ids], content))
|
||||
for session_sudo, notifications in notifications_by_session.items():
|
||||
session_sudo._notify_peers(notifications)
|
||||
|
||||
@http.route("/mail/rtc/session/update_and_broadcast", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def session_update_and_broadcast(self, session_id, values):
|
||||
"""Update a RTC session and broadcasts the changes to the members of its channel,
|
||||
only works of the user is the user of that session.
|
||||
:param int session_id: id of the session to update
|
||||
:param dict values: write dict for the fields to update
|
||||
"""
|
||||
if request.env.user._is_public():
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
if guest:
|
||||
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
|
||||
session = guest.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
|
||||
if session and session.guest_id == guest:
|
||||
session._update_and_broadcast(values)
|
||||
return
|
||||
return
|
||||
# sudo: discuss.channel.rtc.session - only keeping sessions matching the current user
|
||||
session = request.env["discuss.channel.rtc.session"].sudo().browse(int(session_id)).exists()
|
||||
if session and session.partner_id == request.env.user.partner_id:
|
||||
session._update_and_broadcast(values)
|
||||
|
||||
@http.route("/mail/rtc/channel/join_call", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def channel_call_join(self, channel_id, check_rtc_session_ids=None):
|
||||
"""Joins the RTC call of a channel if the user is a member of that channel
|
||||
:param int channel_id: id of the channel to join
|
||||
"""
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - member of current user can join call
|
||||
return member.sudo()._rtc_join_call(check_rtc_session_ids=check_rtc_session_ids)
|
||||
|
||||
@http.route("/mail/rtc/channel/leave_call", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def channel_call_leave(self, channel_id):
|
||||
"""Disconnects the current user from a rtc call and clears any invitation sent to that user on this channel
|
||||
:param int channel_id: id of the channel from which to disconnect
|
||||
"""
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
if not member:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - member of current user can leave call
|
||||
return member.sudo()._rtc_leave_call()
|
||||
|
||||
@http.route("/mail/rtc/channel/cancel_call_invitation", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def channel_call_cancel_invitation(self, channel_id, member_ids=None):
|
||||
"""
|
||||
:param member_ids: members whose invitation is to cancel
|
||||
:type member_ids: list(int) or None
|
||||
"""
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - can cancel invitations in accessible channel
|
||||
return channel.sudo()._rtc_cancel_invitations(member_ids=member_ids)
|
||||
|
||||
@http.route("/mail/rtc/audio_worklet_processor", methods=["GET"], type="http", auth="public")
|
||||
def audio_worklet_processor(self):
|
||||
"""Returns a JS file that declares a WorkletProcessor class in
|
||||
a WorkletGlobalScope, which means that it cannot be added to the
|
||||
bundles like other assets.
|
||||
"""
|
||||
return request.make_response(
|
||||
file_open("mail/static/src/worklets/audio_processor.js", "rb").read(),
|
||||
headers=[
|
||||
("Content-Type", "application/javascript"),
|
||||
("Cache-Control", f"max-age={http.STATIC_CACHE}"),
|
||||
],
|
||||
)
|
||||
|
||||
@http.route("/discuss/channel/ping", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def channel_ping(self, channel_id, rtc_session_id=None, check_rtc_session_ids=None):
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
if not member:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - member of current user can access related sessions
|
||||
channel_member_sudo = member.sudo()
|
||||
if rtc_session_id:
|
||||
domain = [
|
||||
("id", "=", int(rtc_session_id)),
|
||||
("channel_member_id", "=", member.id),
|
||||
]
|
||||
channel_member_sudo.channel_id.rtc_session_ids.filtered_domain(domain).write({}) # update write_date
|
||||
current_rtc_sessions, outdated_rtc_sessions = channel_member_sudo._rtc_sync_sessions(check_rtc_session_ids)
|
||||
return {
|
||||
"rtcSessions": [
|
||||
("ADD", [rtc_session_sudo._mail_rtc_session_format() for rtc_session_sudo in current_rtc_sessions]),
|
||||
(
|
||||
"DELETE",
|
||||
[{"id": missing_rtc_session_sudo.id} for missing_rtc_session_sudo in outdated_rtc_sessions],
|
||||
),
|
||||
]
|
||||
}
|
17
controllers/discuss/voice.py
Normal file
17
controllers/discuss/voice.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open
|
||||
|
||||
|
||||
class VoiceController(http.Controller):
|
||||
|
||||
@http.route("/discuss/voice/worklet_processor", methods=["GET"], type="http", auth="public")
|
||||
def voice_worklet_processor(self):
|
||||
return request.make_response(
|
||||
file_open("mail/static/src/discuss/voice_message/worklets/processor.js", "rb").read(),
|
||||
headers=[
|
||||
("Content-Type", "application/javascript"),
|
||||
],
|
||||
)
|
55
controllers/google_translate.py
Normal file
55
controllers/google_translate.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import babel
|
||||
import requests
|
||||
|
||||
from odoo.http import request, route, Controller
|
||||
|
||||
|
||||
class GoogleTranslateController(Controller):
|
||||
@route("/mail/message/translate", type="json", auth="user")
|
||||
def translate(self, message_id):
|
||||
message = request.env["mail.message"].search([("id", "=", message_id)])
|
||||
if not message:
|
||||
raise request.not_found()
|
||||
domain = [("message_id", "=", message.id), ("target_lang", "=", request.env.user.lang.split("_")[0])]
|
||||
# sudo: mail.message.translation - searching translations of a message that can be read with standard ACL
|
||||
translation = request.env["mail.message.translation"].sudo().search(domain)
|
||||
if not translation:
|
||||
try:
|
||||
source_lang = self._detect_source_lang(message)
|
||||
target_lang = request.env.user.lang.split("_")[0]
|
||||
# sudo: mail.message.translation - create translation of a message that can be read with standard ACL
|
||||
vals = {
|
||||
"body": self._get_translation(str(message.body), source_lang, target_lang),
|
||||
"message_id": message.id,
|
||||
"source_lang": source_lang,
|
||||
"target_lang": target_lang,
|
||||
}
|
||||
translation = request.env["mail.message.translation"].sudo().create(vals)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
return {"error": err.response.json()["error"]["message"]}
|
||||
return {
|
||||
"body": translation.body,
|
||||
"lang_name": babel.Locale(translation.source_lang).get_display_name(request.env.user.lang),
|
||||
}
|
||||
|
||||
def _detect_source_lang(self, message):
|
||||
# sudo: mail.message.translation - searching translations of a message that can be read with standard ACL
|
||||
translation = request.env["mail.message.translation"].sudo().search([("message_id", "=", message.id)], limit=1)
|
||||
if translation:
|
||||
return translation.source_lang
|
||||
response = self._post(endpoint="detect", data={"q": str(message.body)})
|
||||
return response.json()["data"]["detections"][0][0]["language"]
|
||||
|
||||
def _get_translation(self, body, source_lang, target_lang):
|
||||
response = self._post(data={"q": body, "target": target_lang, "source": source_lang})
|
||||
return response.json()["data"]["translations"][0]["translatedText"]
|
||||
|
||||
def _post(self, endpoint="", data=None):
|
||||
# sudo: ir.config_parameter - reading google translate api key, using it to make the request
|
||||
api_key = request.env["ir.config_parameter"].sudo().get_param("mail.google_translate_api_key")
|
||||
url = f"https://translation.googleapis.com/language/translate/v2/{endpoint}?key={api_key}"
|
||||
response = requests.post(url, data=data, timeout=3)
|
||||
response.raise_for_status()
|
||||
return response
|
20
controllers/guest.py
Normal file
20
controllers/guest.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class GuestController(http.Controller):
|
||||
@http.route("/mail/guest/update_name", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_guest_update_name(self, guest_id, name):
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
guest_to_rename_sudo = guest.env["mail.guest"].browse(guest_id).sudo().exists()
|
||||
if not guest_to_rename_sudo:
|
||||
raise NotFound()
|
||||
if guest_to_rename_sudo != guest and not request.env.user._is_admin():
|
||||
raise NotFound()
|
||||
guest_to_rename_sudo._update_name(name)
|
33
controllers/link_preview.py
Normal file
33
controllers/link_preview.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class LinkPreviewController(http.Controller):
|
||||
@http.route("/mail/link_preview", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_link_preview(self, message_id, clear=None):
|
||||
if not request.env["mail.link.preview"]._is_link_preview_enabled():
|
||||
return
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
message = guest.env["mail.message"].search([("id", "=", int(message_id))])
|
||||
if not message:
|
||||
return
|
||||
if not message.is_current_user_or_guest_author and not guest.env.user._is_admin():
|
||||
return
|
||||
if clear:
|
||||
message.sudo().link_preview_ids._unlink_and_notify()
|
||||
guest.env["mail.link.preview"].sudo()._create_from_message_and_notify(message)
|
||||
|
||||
@http.route("/mail/link_preview/delete", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_link_preview_delete(self, link_preview_ids):
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
link_preview_sudo = guest.env["mail.link.preview"].sudo().search([("id", "in", link_preview_ids)])
|
||||
if not link_preview_sudo:
|
||||
return
|
||||
if not link_preview_sudo.message_id.is_current_user_or_guest_author and not guest.env.user._is_admin():
|
||||
return
|
||||
link_preview_sudo._unlink_and_notify()
|
195
controllers/mail.py
Normal file
195
controllers/mail.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.tools import consteq
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailController(http.Controller):
|
||||
_cp_path = '/mail'
|
||||
|
||||
@classmethod
|
||||
def _redirect_to_messaging(cls):
|
||||
url = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
|
||||
return request.redirect(url)
|
||||
|
||||
@classmethod
|
||||
def _check_token(cls, token):
|
||||
base_link = request.httprequest.path
|
||||
params = dict(request.params)
|
||||
params.pop('token', '')
|
||||
valid_token = request.env['mail.thread']._encode_link(base_link, params)
|
||||
return consteq(valid_token, str(token))
|
||||
|
||||
@classmethod
|
||||
def _check_token_and_record_or_redirect(cls, model, res_id, token):
|
||||
comparison = cls._check_token(token)
|
||||
if not comparison:
|
||||
_logger.warning('Invalid token in route %s', request.httprequest.url)
|
||||
return comparison, None, cls._redirect_to_messaging()
|
||||
try:
|
||||
record = request.env[model].browse(res_id).exists()
|
||||
except Exception:
|
||||
record = None
|
||||
redirect = cls._redirect_to_messaging()
|
||||
else:
|
||||
redirect = cls._redirect_to_record(model, res_id)
|
||||
return comparison, record, redirect
|
||||
|
||||
@classmethod
|
||||
def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs):
|
||||
# access_token and kwargs are used in the portal controller override for the Send by email or Share Link
|
||||
# to give access to the record to a recipient that has normally no access.
|
||||
uid = request.session.uid
|
||||
user = request.env['res.users'].sudo().browse(uid)
|
||||
cids = []
|
||||
|
||||
# no model / res_id, meaning no possible record -> redirect to login
|
||||
if not model or not res_id or model not in request.env:
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
# find the access action using sudo to have the details about the access link
|
||||
RecordModel = request.env[model]
|
||||
record_sudo = RecordModel.sudo().browse(res_id).exists()
|
||||
if not record_sudo:
|
||||
# record does not seem to exist -> redirect to login
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
suggested_company = record_sudo._get_mail_redirect_suggested_company()
|
||||
# the record has a window redirection: check access rights
|
||||
if uid is not None:
|
||||
if not RecordModel.with_user(uid).check_access_rights('read', raise_exception=False):
|
||||
return cls._redirect_to_messaging()
|
||||
try:
|
||||
# We need here to extend the "allowed_company_ids" to allow a redirection
|
||||
# to any record that the user can access, regardless of currently visible
|
||||
# records based on the "currently allowed companies".
|
||||
cids_str = request.httprequest.cookies.get('cids', str(user.company_id.id))
|
||||
cids = [int(cid) for cid in cids_str.split(',')]
|
||||
try:
|
||||
record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access_rule('read')
|
||||
except AccessError:
|
||||
# In case the allowed_company_ids from the cookies (i.e. the last user configuration
|
||||
# on their browser) is not sufficient to avoid an ir.rule access error, try to following
|
||||
# heuristic:
|
||||
# - Guess the supposed necessary company to access the record via the method
|
||||
# _get_mail_redirect_suggested_company
|
||||
# - If no company, then redirect to the messaging
|
||||
# - Merge the suggested company with the companies on the cookie
|
||||
# - Make a new access test if it succeeds, redirect to the record. Otherwise,
|
||||
# redirect to the messaging.
|
||||
if not suggested_company:
|
||||
raise AccessError('')
|
||||
cids = cids + [suggested_company.id]
|
||||
record_sudo.with_user(uid).with_context(allowed_company_ids=cids).check_access_rule('read')
|
||||
except AccessError:
|
||||
return cls._redirect_to_messaging()
|
||||
else:
|
||||
record_action = record_sudo._get_access_action(access_uid=uid)
|
||||
else:
|
||||
record_action = record_sudo._get_access_action()
|
||||
if suggested_company:
|
||||
cids = [suggested_company.id]
|
||||
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
|
||||
url_params = {
|
||||
'model': model,
|
||||
'id': res_id,
|
||||
'active_id': res_id,
|
||||
'action': record_action.get('id'),
|
||||
}
|
||||
if cids:
|
||||
url_params['cids'] = cids[0]
|
||||
view_id = record_sudo.get_formview_id()
|
||||
if view_id:
|
||||
url_params['view_id'] = view_id
|
||||
url = '/web/login?redirect=#%s' % url_encode(url_params)
|
||||
return request.redirect(url)
|
||||
|
||||
record_action.pop('target_type', None)
|
||||
# the record has an URL redirection: use it directly
|
||||
if record_action['type'] == 'ir.actions.act_url':
|
||||
return request.redirect(record_action['url'])
|
||||
# other choice: act_window (no support of anything else currently)
|
||||
elif not record_action['type'] == 'ir.actions.act_window':
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
url_params = {
|
||||
'model': model,
|
||||
'id': res_id,
|
||||
'active_id': res_id,
|
||||
'action': record_action.get('id'),
|
||||
}
|
||||
view_id = record_sudo.get_formview_id()
|
||||
if view_id:
|
||||
url_params['view_id'] = view_id
|
||||
|
||||
if cids:
|
||||
url_params['cids'] = ','.join([str(cid) for cid in cids])
|
||||
url = '/web?#%s' % url_encode(url_params)
|
||||
return request.redirect(url)
|
||||
|
||||
@http.route('/mail/view', type='http', auth='public')
|
||||
def mail_action_view(self, model=None, res_id=None, access_token=None, **kwargs):
|
||||
""" Generic access point from notification emails. The heuristic to
|
||||
choose where to redirect the user is the following :
|
||||
|
||||
- find a public URL
|
||||
- if none found
|
||||
- users with a read access are redirected to the document
|
||||
- users without read access are redirected to the Messaging
|
||||
- not logged users are redirected to the login page
|
||||
|
||||
models that have an access_token may apply variations on this.
|
||||
"""
|
||||
# ==============================================================================================
|
||||
# This block of code disappeared on saas-11.3 to be reintroduced by TBE.
|
||||
# This is needed because after a migration from an older version to saas-11.3, the link
|
||||
# received by mail with a message_id no longer work.
|
||||
# So this block of code is needed to guarantee the backward compatibility of those links.
|
||||
if kwargs.get('message_id'):
|
||||
try:
|
||||
message = request.env['mail.message'].sudo().browse(int(kwargs['message_id'])).exists()
|
||||
except:
|
||||
message = request.env['mail.message']
|
||||
if message:
|
||||
model, res_id = message.model, message.res_id
|
||||
# ==============================================================================================
|
||||
|
||||
if res_id and isinstance(res_id, str):
|
||||
try:
|
||||
res_id = int(res_id)
|
||||
except ValueError:
|
||||
res_id = False
|
||||
return self._redirect_to_record(model, res_id, access_token, **kwargs)
|
||||
|
||||
# csrf is disabled here because it will be called by the MUA with unpredictable session at that time
|
||||
@http.route('/mail/unfollow', type='http', auth='public', csrf=False)
|
||||
def mail_action_unfollow(self, model, res_id, pid, token, **kwargs):
|
||||
comparison, record, __ = MailController._check_token_and_record_or_redirect(model, int(res_id), token)
|
||||
if not comparison or not record:
|
||||
raise AccessError(_('Non existing record or wrong token.'))
|
||||
|
||||
pid = int(pid)
|
||||
record_sudo = record.sudo()
|
||||
record_sudo.message_unsubscribe([pid])
|
||||
|
||||
display_link = True
|
||||
if request.session.uid:
|
||||
try:
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule('read')
|
||||
except AccessError:
|
||||
display_link = False
|
||||
|
||||
return request.render('mail.message_document_unfollowed', {
|
||||
'name': record_sudo.display_name,
|
||||
'model_name': request.env['ir.model'].sudo()._get(model).display_name,
|
||||
'access_url': record._notify_get_action_link('view', model=model, res_id=res_id) if display_link else False,
|
||||
})
|
25
controllers/mailbox.py
Normal file
25
controllers/mailbox.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class MailboxController(http.Controller):
|
||||
@http.route("/mail/inbox/messages", methods=["POST"], type="json", auth="user")
|
||||
def discuss_inbox_messages(self, search_term=None, before=None, after=None, limit=30, around=None):
|
||||
partner_id = request.env.user.partner_id.id
|
||||
domain = [("needaction", "=", True)]
|
||||
res = request.env["mail.message"]._message_fetch(domain, search_term=search_term, before=before, after=after, around=around, limit=limit)
|
||||
return {**res, "messages": res["messages"]._message_format_personalize(partner_id)}
|
||||
|
||||
@http.route("/mail/history/messages", methods=["POST"], type="json", auth="user")
|
||||
def discuss_history_messages(self, search_term=None, before=None, after=None, limit=30, around=None):
|
||||
domain = [("needaction", "=", False)]
|
||||
res = request.env["mail.message"]._message_fetch(domain, search_term=search_term, before=before, after=after, around=around, limit=limit)
|
||||
return {**res, "messages": res["messages"].message_format()}
|
||||
|
||||
@http.route("/mail/starred/messages", methods=["POST"], type="json", auth="user")
|
||||
def discuss_starred_messages(self, search_term=None, before=None, after=None, limit=30, around=None):
|
||||
domain = [("starred_partner_ids", "in", [request.env.user.partner_id.id])]
|
||||
res = request.env["mail.message"]._message_fetch(domain, search_term=search_term, before=before, after=after, around=around, limit=limit)
|
||||
return {**res, "messages": res["messages"].message_format()}
|
17
controllers/message_reaction.py
Normal file
17
controllers/message_reaction.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class MessageReactionController(http.Controller):
|
||||
@http.route("/mail/message/reaction", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_message_add_reaction(self, message_id, content, action):
|
||||
message = request.env["mail.message"].browse(int(message_id)).exists()
|
||||
if not message._validate_access_for_current_persona("write"):
|
||||
raise NotFound()
|
||||
message.sudo()._message_reaction(content, action)
|
129
controllers/thread.py
Normal file
129
controllers/thread.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from markupsafe import Markup
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class ThreadController(http.Controller):
|
||||
@http.route("/mail/thread/data", methods=["POST"], type="json", auth="user")
|
||||
def mail_thread_data(self, thread_model, thread_id, request_list):
|
||||
thread = request.env[thread_model].with_context(active_test=False).search([("id", "=", thread_id)])
|
||||
return thread._get_mail_thread_data(request_list)
|
||||
|
||||
@http.route("/mail/thread/messages", methods=["POST"], type="json", auth="user")
|
||||
def mail_thread_messages(self, thread_model, thread_id, search_term=None, before=None, after=None, around=None, limit=30):
|
||||
domain = [
|
||||
("res_id", "=", int(thread_id)),
|
||||
("model", "=", thread_model),
|
||||
("message_type", "!=", "user_notification"),
|
||||
]
|
||||
res = request.env["mail.message"]._message_fetch(domain, search_term=search_term, before=before, after=after, around=around, limit=limit)
|
||||
if not request.env.user._is_public():
|
||||
res["messages"].set_message_done()
|
||||
return {**res, "messages": res["messages"].message_format()}
|
||||
|
||||
@http.route("/mail/partner/from_email", methods=["POST"], type="json", auth="user")
|
||||
def mail_thread_partner_from_email(self, emails, additional_values=None):
|
||||
partners = [
|
||||
{"id": partner.id, "name": partner.name, "email": partner.email}
|
||||
for partner in request.env["res.partner"]._find_or_create_from_emails(emails, additional_values)
|
||||
]
|
||||
return partners
|
||||
|
||||
@http.route("/mail/read_subscription_data", methods=["POST"], type="json", auth="user")
|
||||
def read_subscription_data(self, follower_id):
|
||||
"""Computes:
|
||||
- message_subtype_data: data about document subtypes: which are
|
||||
available, which are followed if any"""
|
||||
request.env["mail.followers"].check_access_rights("read")
|
||||
follower = request.env["mail.followers"].sudo().browse(follower_id)
|
||||
follower.ensure_one()
|
||||
request.env[follower.res_model].check_access_rights("read")
|
||||
record = request.env[follower.res_model].browse(follower.res_id)
|
||||
record.check_access_rule("read")
|
||||
# find current model subtypes, add them to a dictionary
|
||||
subtypes = record._mail_get_message_subtypes()
|
||||
followed_subtypes_ids = set(follower.subtype_ids.ids)
|
||||
subtypes_list = [
|
||||
{
|
||||
"name": subtype.name,
|
||||
"res_model": subtype.res_model,
|
||||
"sequence": subtype.sequence,
|
||||
"default": subtype.default,
|
||||
"internal": subtype.internal,
|
||||
"followed": subtype.id in followed_subtypes_ids,
|
||||
"parent_model": subtype.parent_id.res_model,
|
||||
"id": subtype.id,
|
||||
}
|
||||
for subtype in subtypes
|
||||
]
|
||||
return sorted(
|
||||
subtypes_list,
|
||||
key=lambda it: (it["parent_model"] or "", it["res_model"] or "", it["internal"], it["sequence"]),
|
||||
)
|
||||
|
||||
def _get_allowed_message_post_params(self):
|
||||
return {"attachment_ids", "body", "message_type", "partner_ids", "subtype_xmlid", "parent_id"}
|
||||
|
||||
@http.route("/mail/message/post", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_message_post(self, thread_model, thread_id, post_data, context=None):
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
guest.env["ir.attachment"].browse(post_data.get("attachment_ids", []))._check_attachments_access(
|
||||
post_data.get("attachment_tokens")
|
||||
)
|
||||
if context:
|
||||
request.update_context(**context)
|
||||
canned_response_ids = tuple(cid for cid in post_data.pop('canned_response_ids', []) if isinstance(cid, int))
|
||||
if canned_response_ids:
|
||||
# Avoid serialization errors since last used update is not
|
||||
# essential and should not block message post.
|
||||
request.env.cr.execute("""
|
||||
UPDATE mail_shortcode SET last_used=%(last_used)s
|
||||
WHERE id IN (
|
||||
SELECT id from mail_shortcode WHERE id IN %(ids)s
|
||||
FOR NO KEY UPDATE SKIP LOCKED
|
||||
)
|
||||
""", {
|
||||
'last_used': datetime.now(),
|
||||
'ids': canned_response_ids,
|
||||
})
|
||||
thread = request.env[thread_model].with_context(active_test=False).search([("id", "=", thread_id)])
|
||||
thread = thread.with_context(active_test=True)
|
||||
if not thread:
|
||||
raise NotFound()
|
||||
if "body" in post_data:
|
||||
post_data["body"] = Markup(post_data["body"]) # contains HTML such as @mentions
|
||||
new_partners = []
|
||||
if "partner_emails" in post_data:
|
||||
new_partners = [record.id for record in request.env["res.partner"]._find_or_create_from_emails(
|
||||
post_data["partner_emails"], post_data.get("partner_additional_values", {})
|
||||
)]
|
||||
post_data["partner_ids"] = list(set((post_data.get("partner_ids", [])) + new_partners))
|
||||
message_data = thread.message_post(
|
||||
**{key: value for key, value in post_data.items() if key in self._get_allowed_message_post_params()}
|
||||
).message_format()[0]
|
||||
if "temporary_id" in request.context:
|
||||
message_data["temporary_id"] = request.context["temporary_id"]
|
||||
return message_data
|
||||
|
||||
@http.route("/mail/message/update_content", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_message_update_content(self, message_id, body, attachment_ids, attachment_tokens=None, partner_ids=None):
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
guest.env["ir.attachment"].browse(attachment_ids)._check_attachments_access(attachment_tokens)
|
||||
message_sudo = guest.env["mail.message"].browse(message_id).sudo().exists()
|
||||
if not message_sudo.is_current_user_or_guest_author and not guest.env.user._is_admin():
|
||||
raise NotFound()
|
||||
if not message_sudo.model or not message_sudo.res_id:
|
||||
raise NotFound()
|
||||
body = Markup(body) if body else body # may contain HTML such as @mentions
|
||||
guest.env[message_sudo.model].browse([message_sudo.res_id])._message_update_content(
|
||||
message_sudo, body, attachment_ids=attachment_ids, partner_ids=partner_ids
|
||||
)
|
||||
return message_sudo.message_format()[0]
|
25
controllers/webclient.py
Normal file
25
controllers/webclient.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
|
||||
class WebclientController(http.Controller):
|
||||
@http.route("/mail/init_messaging", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def mail_init_messaging(self):
|
||||
if not request.env.user._is_public():
|
||||
return request.env.user.sudo(False)._init_messaging()
|
||||
guest = request.env["mail.guest"]._get_guest_from_context()
|
||||
if guest:
|
||||
return guest._init_messaging()
|
||||
raise NotFound()
|
||||
|
||||
@http.route("/mail/load_message_failures", methods=["POST"], type="json", auth="user")
|
||||
def mail_load_message_failures(self):
|
||||
# sudo as to not check ACL, which is far too costly
|
||||
# sudo: res.users - return only failures of current user as author
|
||||
return request.env.user.partner_id._message_fetch_failed()
|
19
controllers/webmanifest.py
Normal file
19
controllers/webmanifest.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open
|
||||
from odoo.addons.web.controllers.webmanifest import WebManifest as WebWebManifest
|
||||
|
||||
|
||||
class WebManifest(WebWebManifest):
|
||||
|
||||
def _get_service_worker_content(self):
|
||||
body = super()._get_service_worker_content()
|
||||
|
||||
# Add notification support to the service worker if user but no public
|
||||
if request.env.user.has_group('base.group_user'):
|
||||
with file_open('mail/static/src/service_worker.js') as f:
|
||||
body += f.read()
|
||||
|
||||
return body
|
32
data/discuss_channel_data.xml
Normal file
32
data/discuss_channel_data.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="discuss.channel" id="mail.channel_all_employees">
|
||||
<field name="name">general</field>
|
||||
<field name="description">General announcements for all employees.</field>
|
||||
</record>
|
||||
|
||||
<!-- notify all employees of module installation -->
|
||||
<record model="mail.message" id="mail.module_install_notification">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_all_employees"/>
|
||||
<field name="message_type">email</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="subject">Welcome to Odoo!</field>
|
||||
<field name="body"><![CDATA[<p>Welcome to the #general channel.</p>
|
||||
<p>This channel is accessible to all users to <b>easily share company information</b>.</p>]]></field>
|
||||
</record>
|
||||
|
||||
<record model="discuss.channel.member" id="channel_member_general_channel_for_admin">
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="channel_id" ref="mail.channel_all_employees"/>
|
||||
<field name="fetched_message_id" ref="mail.module_install_notification"/>
|
||||
<field name="seen_message_id" ref="mail.module_install_notification"/>
|
||||
</record>
|
||||
|
||||
<record model="discuss.channel" id="mail.channel_all_employees">
|
||||
<field name="group_ids" eval="[Command.link(ref('base.group_user'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
159
data/discuss_channel_demo.xml
Normal file
159
data/discuss_channel_demo.xml
Normal file
|
@ -0,0 +1,159 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Discussion groups, done in 2 steps to remove creator from followers -->
|
||||
<record model="discuss.channel" id="mail.channel_1">
|
||||
<field name="name">sales</field>
|
||||
<field name="description">Discussion about best sales practices and deals.</field>
|
||||
</record>
|
||||
<record model="discuss.channel" id="mail.channel_2">
|
||||
<field name="name">board-meetings</field>
|
||||
<field name="description">Board meetings, budgets, strategic plans</field>
|
||||
</record>
|
||||
<record model="discuss.channel" id="mail.channel_3">
|
||||
<field name="name">rd</field>
|
||||
<field name="description">Research and development discussion group</field>
|
||||
</record>
|
||||
|
||||
<!-- Best sales practices messages -->
|
||||
<record id="mail_message_channel_1_1" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_1"/>
|
||||
<field name="body"><![CDATA[<p>Selling a training session and selling the products after the training session is more efficient than directly selling a pack with the training session and the products.</p>]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
<record id="mail_message_channel_1_2" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_1"/>
|
||||
<field name="body"><![CDATA[<p>I noted I can not manage efficiently my pipeline when I have more than 50 opportunities in the qualification stage.</p><p>Any advice on this? How do you organize your activities with more than 50 opportunities?</p>]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_root"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=4)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
<record id="mail_message_channel_1_2_1" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_1"/>
|
||||
<field name="body"><![CDATA[<p>When I have too much opportunities in the pipe, I start communicating with prospects more by email than phonecalls.</p><p>I send an email to create a sense of emergency, like <i>"can I call you this week about our quote?"</i> and I call only those that answer this email.</p><p>You can use the email template feature of Odoo to automate email composition.</p>]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="parent_id" ref="mail_message_channel_1_2"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
|
||||
<!-- Pushed to all employees -->
|
||||
<record id="mail_message_channel_whole_1" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_all_employees"/>
|
||||
<field name="body"><![CDATA[
|
||||
<p>
|
||||
Great news!<br/>
|
||||
Our company has received the Deloitte Fast 50 award. We are the fastest
|
||||
growing company of the country, with a growth of 1549% over the past 5
|
||||
years. You can get more information <a href="http://www.openerp.com/node/1244/2012/10">on our blog</a>.
|
||||
</p>
|
||||
]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(minutes=22)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
<record id="mail_message_channel_whole_2" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_all_employees"/>
|
||||
<field name="body"><![CDATA[<p>Your monthly meal vouchers arrived. You can get them at the HR's office.</p>
|
||||
<p>This month you also get 250 EUR of eco-vouchers if you have been in the company for more than a year.</p>]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
<record id="mail_message_channel_whole_2_1" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="channel_all_employees"/>
|
||||
<field name="body"><![CDATA[<p>Thanks! Could you please remind me where is Christine's office, if I may ask? I'm new here!</p>]]></field>
|
||||
<field name="parent_id" ref="mail_message_channel_whole_2"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_root"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(minutes=34)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
<record id="mail_message_channel_whole_2_2" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="channel_all_employees"/>
|
||||
<field name="body"><![CDATA[<p>Building B3, second floor to the right :-).</p>]]></field>
|
||||
<field name="parent_id" ref="mail_message_channel_whole_2_1"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(minutes=22)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<!-- Board messages -->
|
||||
<record id="mail_message_channel_2_1" model="mail.message">
|
||||
<field name="model">discuss.channel</field>
|
||||
<field name="res_id" ref="mail.channel_2"/>
|
||||
<field name="body"><![CDATA[
|
||||
<p>
|
||||
Dear Board Members,
|
||||
</p>
|
||||
<p>
|
||||
The main events of the month of October are:
|
||||
</p>
|
||||
<p>
|
||||
<b>Sales:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Invoicing is respectively of 442k€ for our European company (66% of the budget) and $404k for the U.S. office (75% of the budget). Despite these numbers that are far below our initial expectations, the growth of the month of October is 51% compared to last year.</li>
|
||||
<li>The month of September having been better than our initial forecasts, the consolidated yearly revenue is only of $20k below our forecast made during the board of September.</li>
|
||||
<li>The consolidated forecast for the end of the year is $6.749k, which is a growth of 76% compared to last year and an achievement of 87% of the budget.</li>
|
||||
<li>The recruitment of new resellers has been very good, especially in Europe, where we signed 30 new resellers this month.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>Finance :</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>The profit and loss has been negatively impacted this month by revenues that are far beyond the budget and charges that are 15% above the budget. The main extra we had in our charges this month is due to the provisioning of the salaries for the holidays period, $50k.</li>
|
||||
<li>We also got the payment of our long awaited subsidies, the cash level has increased of 300K€ which gives a current balance of 963 K€ without including the straight loan of 350 K€.</li>
|
||||
<li>The aged customer balance has been similar to the one of the last month with a small decrease of the DSO. We have recruited a new accountant assistant for the credit collection. She is mostly doing phone calls for all invoices that are due since 30 days, so we should get improvements of the DSO in November. The sum of the invoicing on which we have a risk in the aged customer balance is 100K€.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>Resellers and Customers:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>The total number of resellers is 429, across 87 countries.</li>
|
||||
<li>The total number of installations of our software increased to 37K, against 33K for the month of September but we still did not reached the highest level we reached during this year (44K in march and may)</li>
|
||||
<li>We have passed the 10000th customer in production with 10271 customers at the end of October. The paying customer ratio is 6,6%.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>Launch of the new release:</b>
|
||||
</p>
|
||||
<p>
|
||||
We are working actively on the new release which is scheduled for the end of November.
|
||||
</p>
|
||||
<ul>
|
||||
<li>We will publish the release note this week</li>
|
||||
<li>The whole Sales Team will be trained on the new version this Friday</li>
|
||||
<li>We will do a public announce to our resellers the 21th of November. We plan to show them: a description of the new features, the new distribution strategy, the new pricing and the communication plan.</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<p>
|
||||
Nicolas, can you book a meeting room for our meeting of Friday 2pm?
|
||||
</p>
|
||||
<p>
|
||||
Regards.
|
||||
</p>
|
||||
]]></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
9
data/ir_config_parameter_data.xml
Normal file
9
data/ir_config_parameter_data.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="gc_delete_overdue_activities_year_threshold" model="ir.config_parameter">
|
||||
<field name="key">mail.activity.gc.delete_overdue_years</field>
|
||||
<field name="value">3</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
92
data/ir_cron_data.xml
Normal file
92
data/ir_cron_data.xml
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record forcecreate="True" id="ir_cron_mail_scheduler_action" model="ir.cron">
|
||||
<field name="name">Mail: Email Queue Manager</field>
|
||||
<field name="model_id" ref="model_mail_mail"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.process_email_queue()</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_module_update_notification" model="ir.cron">
|
||||
<field name="name">Publisher: Update Notification</field>
|
||||
<field name="model_id" ref="model_publisher_warranty_contract"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.update_notification(None)</field>
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">weeks</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="nextcall" eval="(DateTime.now() + timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')" />
|
||||
<field eval="False" name="doall" />
|
||||
<field name="priority">1000</field>
|
||||
</record>
|
||||
|
||||
<record id="base.ir_cron_act" model="ir.actions.act_window">
|
||||
<field name="domain" eval="[('id','!=', ref('mail.ir_cron_module_update_notification'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_delete_notification" model="ir.cron">
|
||||
<field name="name">Notification: Delete Notifications older than 6 Month</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_mail_notification"/>
|
||||
<field name="code">model._gc_notifications(max_age_days=180)</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_mail_gateway_action" model="ir.cron">
|
||||
<field name="name">Mail: Fetchmail Service</field>
|
||||
<field name="model_id" ref="model_fetchmail_server"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._fetch_mails()</field>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<!-- Active flag is set on fetchmail_server.create/write -->
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_send_scheduled_message" model="ir.cron">
|
||||
<field name="name">Notification: Send scheduled message notifications</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_mail_message_schedule"/>
|
||||
<field name="code">model._send_notifications_cron()</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_web_push_notification" model="ir.cron">
|
||||
<field name="name">Mail: send web push notification</field>
|
||||
<field name="model_id" ref="model_mail_notification_web_push"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._push_notification_to_endpoint()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_discuss_channel_member_unmute" model="ir.cron">
|
||||
<field name="name">Discuss: channel member unmute</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_discuss_channel_member"/>
|
||||
<field name="code">model._unmute()</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
43
data/mail_activity_data.xml
Normal file
43
data/mail_activity_data.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mail_activity_data_email" model="mail.activity.type">
|
||||
<field name="name">Email</field>
|
||||
<field name="icon">fa-envelope</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
<record id="mail_activity_data_call" model="mail.activity.type">
|
||||
<field name="name">Call</field>
|
||||
<field name="icon">fa-phone</field>
|
||||
<field name="category">phonecall</field>
|
||||
<field name="delay_count">2</field>
|
||||
<field name="sequence">6</field>
|
||||
</record>
|
||||
<record id="mail_activity_data_meeting" model="mail.activity.type">
|
||||
<field name="name">Meeting</field>
|
||||
<field name="icon">fa-users</field>
|
||||
<field name="sequence">9</field>
|
||||
</record>
|
||||
<record id="mail_activity_data_todo" model="mail.activity.type">
|
||||
<field name="name">To-Do</field>
|
||||
<field name="icon">fa-tasks</field>
|
||||
<field name="delay_count">5</field>
|
||||
<field name="sequence">12</field>
|
||||
</record>
|
||||
<record id="mail_activity_data_upload_document" model="mail.activity.type">
|
||||
<field name="name">Upload Document</field>
|
||||
<field name="icon">fa-upload</field>
|
||||
<field name="delay_count">5</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="category">upload_file</field>
|
||||
</record>
|
||||
<record id="mail_activity_data_warning" model="mail.activity.type">
|
||||
<field name="name">Exception</field>
|
||||
<field name="icon">fa-warning</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="sequence">99</field>
|
||||
<field name="decoration_type">warning</field>
|
||||
<field name="active">False</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
25
data/mail_groups.xml
Normal file
25
data/mail_groups.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="group_mail_template_editor" model="res.groups">
|
||||
<field name="name">Mail Template Editor</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_system" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('mail.group_mail_template_editor'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- By default, allow all users to edit mail templates -->
|
||||
<record id="base.group_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('mail.group_mail_template_editor'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Group used for the notification_type field of res.users -->
|
||||
<record id="group_mail_notification_type_inbox" model="res.groups">
|
||||
<field name="name">Receive notifications in Odoo</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
24
data/mail_message_subtype_data.xml
Normal file
24
data/mail_message_subtype_data.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<!-- Discussion subtype for messaging / Chatter -->
|
||||
<record id="mt_comment" model="mail.message.subtype">
|
||||
<field name="name">Discussions</field>
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="track_recipients" eval="True"/>
|
||||
</record>
|
||||
<record id="mt_note" model="mail.message.subtype">
|
||||
<field name="name">Note</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="sequence" eval="100"/>
|
||||
<field name="track_recipients" eval="True"/>
|
||||
</record>
|
||||
<record id="mt_activities" model="mail.message.subtype">
|
||||
<field name="name">Activities</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="sequence" eval="90"/>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
59
data/mail_templates_chatter.xml
Normal file
59
data/mail_templates_chatter.xml
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Discuss utility templates for notifications -->
|
||||
<template id="message_user_assigned">
|
||||
<span>Dear <t t-esc="object.user_id.sudo().name"/>,</span>
|
||||
<br/><br/>
|
||||
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="model_description or 'document'"/> <t t-esc="object.display_name"/>.</span>
|
||||
<br/>
|
||||
</template>
|
||||
|
||||
<template id="message_activity_done">
|
||||
<div>
|
||||
<p>
|
||||
<span t-attf-class="fa #{activity.activity_type_id.icon} fa-fw"/><span t-field="activity.activity_type_id.name"/> done
|
||||
<t t-if="display_assignee"> (originally assigned to <span t-field="activity.user_id.name"/>)</t>
|
||||
<span t-if="activity.summary">: </span><span t-if="activity.summary" t-field="activity.summary"/>
|
||||
</p>
|
||||
<div t-if="feedback">
|
||||
<t t-foreach="feedback.split('\n')" t-as="feedback_line">
|
||||
<t t-esc="feedback_line"/>
|
||||
<br t-if="not feedback_line_last"/>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="activity.note and activity.note != '<p><br></p>'"><!-- <p></br></p> -->
|
||||
<div class="o_mail_note_title"><strong>Original note:</strong></div>
|
||||
<div t-field="activity.note"/>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="message_activity_assigned">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <span t-field="activity.user_id.name"/>,
|
||||
<br/><br/>
|
||||
<p>
|
||||
<span t-field="activity.create_uid.name"/> has just assigned you the following activity:
|
||||
<ul>
|
||||
<li>Document: "<t t-esc="activity.res_name"/>"
|
||||
<t t-if="model_description"> (<t t-esc="model_description"/>)</t>
|
||||
</li>
|
||||
<li t-if="activity.summary">Summary: <span t-field="activity.summary"/></li>
|
||||
<li>Deadline: <span t-field="activity.date_deadline"/></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="message_origin_link">
|
||||
<p>
|
||||
<t t-if="edit">This <t t-esc="self.env['ir.model']._get(self._name).name.lower()"/> has been modified from:</t>
|
||||
<t t-else="">This <t t-esc="self.env['ir.model']._get(self._name).name.lower()"/> has been created from:</t>
|
||||
<t t-foreach="origin" t-as="o">
|
||||
<a href="#" t-att-data-oe-model="o._name" t-att-data-oe-id="o.id"> <t t-esc="o.display_name"/></a><span t-if="origin.ids[-1:] != o.ids">, </span>
|
||||
</t>
|
||||
</p>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
213
data/mail_templates_email_layouts.xml
Normal file
213
data/mail_templates_email_layouts.xml
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="mail_notification_layout" name="Mail: mail notification layout template">
|
||||
<html t-att-lang="lang">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
</head>
|
||||
<body style="font-family:Verdana, Arial,sans-serif; color: #454748;">
|
||||
<t t-set="subtype_internal" t-value="subtype and subtype.internal"/>
|
||||
<!-- HEADER -->
|
||||
<t t-call="mail.notification_preview"/>
|
||||
<div style="max-width: 900px; width: 100%;">
|
||||
<div t-if="has_button_access" itemscope="itemscope" itemtype="http://schema.org/EmailMessage">
|
||||
<div itemprop="potentialAction" itemscope="itemscope" itemtype="http://schema.org/ViewAction">
|
||||
<link itemprop="target" t-att-href="button_access['url']"/>
|
||||
<link itemprop="url" t-att-href="button_access['url']"/>
|
||||
<meta itemprop="name" t-att-content="button_access['title']"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="subtitles or has_button_access or actions or not is_discussion"
|
||||
summary="o_mail_notification" style="padding: 0px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: 100%; margin-top: 5px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="center" t-if="company and not company.uses_default_logo">
|
||||
<img t-att-src="'/logo.png?company=%s' % company.id" style="padding: 0px; margin: 0px; height: auto; max-width: 200px; max-height: 36px;" t-att-alt="'%s' % company.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="center">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 10px 0px;"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="center" style="white-space:nowrap;">
|
||||
<table cellspacing="0" cellpadding="0" border="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td t-if="has_button_access" t-att-style="'border-radius: 3px; text-align: center; background: ' + (company.email_secondary_color or '#875A7B') + ';'">
|
||||
<a t-att-href="button_access['url']" style="font-size: 12px; color: #FFFFFF; display: block; padding: 8px 12px 11px; text-decoration: none !important; font-weight: 400;">
|
||||
<t t-out="button_access['title']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="has_button_access">&nbsp;&nbsp;</td>
|
||||
|
||||
<td t-if="actions">
|
||||
<t t-foreach="actions" t-as="action">
|
||||
<a t-att-href="action['url']" t-att-style="'font-size: 12px; color: ' + (company.email_secondary_color or '#875A7B')+ '; text-decoration:none !important;'">
|
||||
<t t-out="action['title']"/>
|
||||
</a>
|
||||
&nbsp;&nbsp;
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="subtitles" style="font-size: 12px;">
|
||||
<t t-foreach="subtitles" t-as="subtitle">
|
||||
<span t-attf-style="{{ 'font-weight:bold;' if subtitle_first else '' }}"
|
||||
t-out="subtitle"/>
|
||||
<br t-if="not subtitle_last"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-else=""><span style="font-weight:bold;" t-out="record_name"/><br/></td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="center">
|
||||
<hr width="100%"
|
||||
style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0;margin: 10px 0px;"/>
|
||||
<p t-if="subtype_internal" style="background-color: #f2dede; padding: 5px; margin-bottom: 16px; font-size: 13px;">
|
||||
<strong>Internal communication</strong>: Replying will post an internal note. Followers won't receive any email notification.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- CONTENT -->
|
||||
<div t-out="message.body" style="font-size: 13px;"/>
|
||||
<ul t-if="tracking_values">
|
||||
<t t-foreach="tracking_values" t-as="tracking">
|
||||
<li><t t-out="tracking[0]"/>: <t t-out="tracking[1]"/> → <t t-out="tracking[2]"/></li>
|
||||
</t>
|
||||
</ul>
|
||||
<t class="o_signature">
|
||||
<div t-if="email_add_signature and not is_html_empty(signature)" t-out="signature" style="font-size: 13px;"/>
|
||||
</t>
|
||||
<!-- FOOTER -->
|
||||
<div style="margin-top:32px;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 4px 0px;"/>
|
||||
<b t-out="company.name" style="font-size:11px;"/><br/>
|
||||
<p style="color: #999999; margin-top:2px; font-size:11px;">
|
||||
<t t-out="company.phone"/>
|
||||
<t t-if="company.email and company.phone"> |</t>
|
||||
<a t-if="company.email" t-att-href="'mailto:%s' % company.email" style="text-decoration:none; color: #999999;" t-out="company.email"/>
|
||||
<t t-if="company.website and (company.phone or company.email)"> |</t>
|
||||
<a t-if="company.website" t-att-href="'%s' % company.website" style="text-decoration:none; color: #999999;" t-out="company.website"/>
|
||||
</p>
|
||||
</div>
|
||||
<div style="color: #555555; font-size:11px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=email"
|
||||
t-att-style="'color: ' + (company.email_secondary_color or '#875A7B') + ';'">Odoo</a>
|
||||
<span id="mail_unfollow">
|
||||
| <a href="/mail/unfollow" style="text-decoration:none; color:#555555;">Unfollow</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
</template>
|
||||
|
||||
<template id="mail_notification_light">
|
||||
<html t-att-lang="lang">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<t t-set="subtype_internal" t-value="False"/>
|
||||
<t t-call="mail.notification_preview"/>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 24px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; padding: 0; border-collapse:separate;">
|
||||
<tr><td valign="middle">
|
||||
<span style="font-size: 10px;">Your <t t-out="model_description or 'document'"/></span>
|
||||
<br/>
|
||||
<t t-if="has_button_access">
|
||||
<a t-att-href="button_access['url']">
|
||||
<span style="font-size: 20px; font-weight: bold;">
|
||||
<t t-out="message.record_name and message.record_name.replace('/','-') or ''"/>
|
||||
</span>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="font-size: 20px; font-weight: bold;">
|
||||
<t t-out="message.record_name and message.record_name.replace('/','-') or ''"/>
|
||||
</span>
|
||||
</t>
|
||||
</td><td valign="middle" align="right" t-if="company and not company.uses_default_logo">
|
||||
<img t-att-src="'/logo.png?company=%s' % company.id" style="padding: 0px; margin: 0px; height: 48px;" t-att-alt="'%s' % company.name"/>
|
||||
</td></tr>
|
||||
<tr><td colspan="2" style="text-align:center;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:4px 0px 32px 0px;"/>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td style="min-width: 590px;">
|
||||
<t t-out="message.body"/>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px; padding: 0 8px 0 8px; font-size:11px;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 4px 0px;"/>
|
||||
<b t-out="company.name"/><br/>
|
||||
<div style="color: #999999;">
|
||||
<t t-out="company.phone"/>
|
||||
<t t-if="company.email and company.phone"> |</t>
|
||||
<a t-if="company.email" t-att-href="'mailto:%s' % company.email" style="text-decoration:none; color: #999999;" t-out="company.email"/>
|
||||
<t t-if="company.website and (company.phone or company.email)"> |</t>
|
||||
<a t-if="company.website" t-att-href="'%s' % company.website" style="text-decoration:none; color: #999999;" t-out="company.website"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- POWERED BY -->
|
||||
<tr><td align="center" style="min-width: 590px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=email"
|
||||
t-att-style="'color: ' + (company.email_secondary_color or '#875A7B') + ';'">Odoo</a>
|
||||
<span id="mail_unfollow">
|
||||
| <a href="/mail/unfollow" style="text-decoration:none; color:#555555;">Unfollow</a>
|
||||
</span>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="notification_preview">
|
||||
<div style="display: none; max-height: 0px; overflow: hidden; color:#fff; font-size:0px; line-height:0px">
|
||||
<t t-if="tracking_values">
|
||||
<t t-out="tracking_values[0][0]"/>: <t t-out="tracking_values[0][1]"/> → <t t-out="tracking_values[0][2]"/>
|
||||
<t t-if="len(tracking_values) > 1"> |...</t>
|
||||
<t t-if="message.preview"> | </t>
|
||||
</t>
|
||||
<t t-if="subtype_internal">Internal communication: </t><t t-out="message.preview"/>
|
||||
<!--Trailing whitespace to push back email content so that it doesn't appear in preview. Specific characters to use may change over time -->
|
||||
<t t-out="'͏ ​ ' * 140"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="mail_notification_layout_with_responsible_signature"
|
||||
name="Mail: mail notification layout with responsible signature (user_id of the record)"
|
||||
inherit_id="mail.mail_notification_layout" primary="True">
|
||||
<xpath expr="//t[hasclass('o_signature')]" position="replace">
|
||||
<t class="o_signature">
|
||||
<div t-if="email_add_signature and 'user_id' in record and record.user_id and not record.env.user._is_superuser() and not is_html_empty(record.user_id.sudo().signature)"
|
||||
t-out="record.user_id.sudo().signature" style="font-size: 13px;"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
33
data/mail_templates_mailgateway.xml
Normal file
33
data/mail_templates_mailgateway.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="message_notification_limit_email">
|
||||
<p>Dear Sender,</p>
|
||||
<p>
|
||||
The message below could not be accepted by the address <t t-esc="email"/> because you have
|
||||
contacted it too many times in the last few minutes.
|
||||
<br/>
|
||||
Please try again later.
|
||||
</p>
|
||||
<p>Kind Regards</p>
|
||||
</template>
|
||||
|
||||
<template id="mail_bounce_catchall">
|
||||
<div>
|
||||
<p>Hello <t t-esc="message['email_from']"/>,</p>
|
||||
<p>The email sent to <t t-esc="message['to']"/> cannot be processed. This address
|
||||
is used to collect replies and should not be used to directly contact <t t-esc="res_company.name"/>.</p>
|
||||
<p>Please contact us instead using <a t-att-href="'mailto:%s' % res_company.email"><t t-esc="res_company.email"/></a></p>
|
||||
<p>Regards,</p>
|
||||
<p>The <t t-esc="res_company.name"/> team.</p>
|
||||
</div>
|
||||
<blockquote><t t-esc="message['body']"/></blockquote>
|
||||
</template>
|
||||
|
||||
<!-- Mail bounce alias mail template -->
|
||||
<template id="mail_bounce_alias_security">
|
||||
<div><t t-out="body"/></div>
|
||||
<blockquote><t t-out="message['body']"/></blockquote>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
15
data/neutralize.sql
Normal file
15
data/neutralize.sql
Normal file
|
@ -0,0 +1,15 @@
|
|||
-- deactivate mail template
|
||||
UPDATE mail_template
|
||||
SET mail_server_id = NULL;
|
||||
-- deactivate fetchmail server
|
||||
UPDATE fetchmail_server
|
||||
SET active = false;
|
||||
|
||||
-- reset WEB Push Notification:
|
||||
-- * delete VAPID/JWT keys
|
||||
DELETE FROM ir_config_parameter
|
||||
WHERE key IN ('mail.web_push_vapid_private_key', 'mail.web_push_vapid_public_key', 'mail.sfu_server_key');
|
||||
-- * delete delayed messages (CRON)
|
||||
TRUNCATE mail_notification_web_push;
|
||||
-- * delete Devices for each partners
|
||||
TRUNCATE mail_partner_device CASCADE;
|
14
data/res_partner_data.xml
Normal file
14
data/res_partner_data.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<record id="base.partner_root" model="res.partner">
|
||||
<field name="name">OdooBot</field>
|
||||
<field name="image_1920" type="base64" file="mail/static/src/img/odoobot.png"/>
|
||||
</record>
|
||||
|
||||
<!-- user root should not receive emails at creation -->
|
||||
<record id="base.user_root" model="res.users">
|
||||
<field name="notification_type">inbox</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
37
data/security_notifications_templates.xml
Normal file
37
data/security_notifications_templates.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Template for security update notification (password/login/mail changed, ...) -->
|
||||
<template id="account_security_setting_update">
|
||||
<p>
|
||||
<span>Dear <t t-out="user.name"/>,</span>
|
||||
</p>
|
||||
<p>
|
||||
<span t-out="security_update_text"/>
|
||||
<span>(<t
|
||||
t-out="update_datetime"
|
||||
t-options='{"widget": "datetime", "hide_seconds": True}'/>).</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>If this was done by you:</span><br/>
|
||||
<ul>
|
||||
<li>You can safely ignore this message</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
<span>If this was not done by you:</span>
|
||||
</p>
|
||||
<ul class="o_mail_account_security_suggestions">
|
||||
<li t-if="suggest_password_reset">
|
||||
<span>We suggest you start by</span>
|
||||
<a t-att-href="password_reset_url">
|
||||
Resetting Your Password
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
Contact your administrator
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
32038
i18n/af.po
Normal file
32038
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
32034
i18n/am.po
Normal file
32034
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
11258
i18n/ar.po
Normal file
11258
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
32047
i18n/az.po
Normal file
32047
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
11004
i18n/bg.po
Normal file
11004
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
32050
i18n/bs.po
Normal file
32050
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
11181
i18n/ca.po
Normal file
11181
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
11064
i18n/cs.po
Normal file
11064
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
11061
i18n/da.po
Normal file
11061
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
11408
i18n/de.po
Normal file
11408
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
32063
i18n/el.po
Normal file
32063
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/en_GB.po
Normal file
32037
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
11381
i18n/es.po
Normal file
11381
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
11382
i18n/es_419.po
Normal file
11382
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_BO.po
Normal file
32037
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_CL.po
Normal file
32037
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_CO.po
Normal file
32037
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_CR.po
Normal file
32037
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_DO.po
Normal file
32037
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_EC.po
Normal file
32037
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_PE.po
Normal file
32037
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_PY.po
Normal file
32037
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/es_VE.po
Normal file
32037
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
11027
i18n/et.po
Normal file
11027
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/eu.po
Normal file
32037
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
10951
i18n/fa.po
Normal file
10951
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
11326
i18n/fi.po
Normal file
11326
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/fo.po
Normal file
32037
i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
11377
i18n/fr.po
Normal file
11377
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/fr_CA.po
Normal file
32037
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/gl.po
Normal file
32037
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
32050
i18n/gu.po
Normal file
32050
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
10978
i18n/he.po
Normal file
10978
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
32077
i18n/hr.po
Normal file
32077
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
11006
i18n/hu.po
Normal file
11006
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
10950
i18n/hy.po
Normal file
10950
i18n/hy.po
Normal file
File diff suppressed because it is too large
Load Diff
11329
i18n/id.po
Normal file
11329
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
10877
i18n/is.po
Normal file
10877
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
11351
i18n/it.po
Normal file
11351
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
11047
i18n/ja.po
Normal file
11047
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/ka.po
Normal file
32037
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/kab.po
Normal file
32037
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
32042
i18n/km.po
Normal file
32042
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
11056
i18n/ko.po
Normal file
11056
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
32034
i18n/lb.po
Normal file
32034
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
32037
i18n/lo.po
Normal file
32037
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user