mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 21:52:00 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
54
odoo-bringout-oca-ocb-mail/mail/README.md
Normal file
54
odoo-bringout-oca-ocb-mail/mail/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.
|
||||
7
odoo-bringout-oca-ocb-mail/mail/__init__.py
Normal file
7
odoo-bringout-oca-ocb-mail/mail/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- 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
|
||||
238
odoo-bringout-oca-ocb-mail/mail/__manifest__.py
Normal file
238
odoo-bringout-oca-ocb-mail/mail/__manifest__.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Discuss',
|
||||
'version': '1.10',
|
||||
'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_blacklist_remove_views.xml',
|
||||
'wizard/mail_compose_message_views.xml',
|
||||
'wizard/mail_resend_message_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_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/mail_channel_member_views.xml',
|
||||
'views/mail_channel_rtc_session_views.xml',
|
||||
'views/mail_link_preview_views.xml',
|
||||
'views/mail_channel_views.xml',
|
||||
'views/mail_shortcode_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'views/res_config_settings_views.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/mail_channel_data.xml',
|
||||
'data/mail_activity_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'security/mail_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/discuss_public_templates.xml',
|
||||
'views/mail_alias_views.xml',
|
||||
'views/mail_gateway_allowed_views.xml',
|
||||
'views/mail_guest_views.xml',
|
||||
'views/mail_message_reaction_views.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',
|
||||
],
|
||||
'demo': [
|
||||
'data/mail_channel_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'assets': {
|
||||
'mail.assets_core_messaging': [
|
||||
'mail/static/src/model/*.js',
|
||||
'mail/static/src/core_models/*.js',
|
||||
],
|
||||
'mail.assets_messaging': [
|
||||
('include', 'mail.assets_core_messaging'),
|
||||
'mail/static/src/models/*.js',
|
||||
'mail/static/lib/selfie_segmentation/selfie_segmentation.js',
|
||||
],
|
||||
'mail.assets_model_data': [
|
||||
'mail/static/src/models_data/*.js',
|
||||
],
|
||||
# Custom bundle in case we want to remove things that are later added to web.assets_common
|
||||
'mail.assets_common_discuss_public': [
|
||||
('include', 'web.assets_common'),
|
||||
],
|
||||
'mail.assets_discuss_public': [
|
||||
# SCSS dependencies (the order is important)
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/src/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/scss/pre_variables.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web/static/src/scss/import_bootstrap.scss',
|
||||
'web/static/src/scss/utilities_custom.scss',
|
||||
'web/static/lib/bootstrap/scss/utilities/_api.scss',
|
||||
'web/static/src/scss/bootstrap_review.scss',
|
||||
'web/static/src/webclient/webclient.scss',
|
||||
'web/static/src/core/utils/*.scss',
|
||||
# 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/composer.scss',
|
||||
# Dependency of notification_group, notification_request, thread_needaction_preview and thread_preview
|
||||
'mail/static/src/components/notification_list/notification_list_item.scss',
|
||||
'mail/static/src/component_hooks/*.js',
|
||||
'mail/static/src/components/*/*',
|
||||
|
||||
('remove', 'mail/static/src/**/*.dark.scss'),
|
||||
# Unused by guests and depends on ViewDialogs, better to remove it instead of pulling the whole view dependency tree
|
||||
('remove', 'mail/static/src/components/composer_suggested_recipient/*'),
|
||||
('remove', 'mail/static/src/components/activity_menu_container/*'),
|
||||
'mail/static/src/js/emojis.js',
|
||||
'mail/static/src/js/utils.js',
|
||||
('include', 'mail.assets_messaging'),
|
||||
'mail/static/src/public/*',
|
||||
'mail/static/src/services/*.js',
|
||||
('remove', 'mail/static/src/services/systray_service.js'),
|
||||
'mail/static/src/utils/*.js',
|
||||
# Framework JS
|
||||
'web/static/lib/luxon/luxon.js',
|
||||
'web/static/src/core/**/*',
|
||||
# FIXME: debug menu currently depends on webclient, once it doesn't we don't need to remove the contents of the debug folder
|
||||
('remove', 'web/static/src/core/debug/**/*'),
|
||||
'web/static/src/env.js',
|
||||
'web/static/src/legacy/js/core/misc.js',
|
||||
'web/static/src/legacy/js/env.js',
|
||||
'web/static/src/legacy/js/fields/field_utils.js',
|
||||
'web/static/src/legacy/js/owl_compatibility.js',
|
||||
'web/static/src/legacy/js/services/data_manager.js',
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
'web/static/src/legacy/js/widgets/date_picker.js',
|
||||
'web/static/src/legacy/legacy_load_views.js',
|
||||
'web/static/src/legacy/legacy_promise_error_handler.js',
|
||||
'web/static/src/legacy/legacy_rpc_error_handler.js',
|
||||
'web/static/src/legacy/utils.js',
|
||||
'web/static/src/legacy/xml/base.xml',
|
||||
],
|
||||
'web._assets_primary_variables': [
|
||||
'mail/static/src/scss/variables/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',
|
||||
# defines mixins and variables used by multiple components
|
||||
'mail/static/src/components/notification_list/notification_list_item.scss',
|
||||
'mail/static/src/js/**/*.js',
|
||||
'mail/static/src/utils/*.js',
|
||||
'mail/static/src/scss/*.scss',
|
||||
'mail/static/src/xml/*.xml',
|
||||
'mail/static/src/component_hooks/*.js',
|
||||
'mail/static/src/backend_components/*/*',
|
||||
'mail/static/src/components/*/*.js',
|
||||
'mail/static/src/components/*/*.scss',
|
||||
'mail/static/src/components/*/*.xml',
|
||||
'mail/static/src/views/*/*.xml',
|
||||
('include', 'mail.assets_messaging'),
|
||||
'mail/static/src/services/*.js',
|
||||
'mail/static/src/views/**/*.js',
|
||||
'mail/static/src/views/**/*.xml',
|
||||
'mail/static/src/views/**/*.scss',
|
||||
'mail/static/src/webclient/commands/*.js',
|
||||
'mail/static/src/widgets/*/*.js',
|
||||
'mail/static/src/widgets/*/*.scss',
|
||||
|
||||
# Don't include dark mode files in light mode
|
||||
('remove', 'mail/static/src/components/*/*.dark.scss'),
|
||||
],
|
||||
"web.dark_mode_assets_backend": [
|
||||
'mail/static/src/components/*/*.dark.scss',
|
||||
],
|
||||
'web.assets_backend_prod_only': [
|
||||
'mail/static/src/main.js',
|
||||
],
|
||||
'mail.assets_discuss_public_test_tours': [
|
||||
'mail/static/tests/tours/discuss_public_tour.js',
|
||||
'mail/static/tests/tours/mail_channel_as_guest_tour.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'mail/static/tests/tours/**/*',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'mail/static/tests/helpers/**/*.js',
|
||||
'mail/static/tests/models/*.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'mail/static/tests/qunit_suite_tests/**/*.js',
|
||||
],
|
||||
'web.qunit_mobile_suite_tests': [
|
||||
'mail/static/tests/qunit_mobile_suite_tests/**/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
Binary file not shown.
7
odoo-bringout-oca-ocb-mail/mail/controllers/__init__.py
Normal file
7
odoo-bringout-oca-ocb-mail/mail/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import bus
|
||||
from . import discuss
|
||||
from . import home
|
||||
from . import mail
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
52
odoo-bringout-oca-ocb-mail/mail/controllers/bus.py
Normal file
52
odoo-bringout-oca-ocb-mail/mail/controllers/bus.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import SUPERUSER_ID, tools
|
||||
from odoo.http import request, route
|
||||
from odoo.addons.bus.controllers.main import BusController
|
||||
|
||||
|
||||
class MailChatController(BusController):
|
||||
|
||||
def _default_request_uid(self):
|
||||
""" For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none)
|
||||
!!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access)
|
||||
can access to the resource.
|
||||
"""
|
||||
return request.session.uid and request.session.uid or SUPERUSER_ID
|
||||
|
||||
# --------------------------
|
||||
# Anonymous routes (Common Methods)
|
||||
# --------------------------
|
||||
@route('/mail/chat_post', type="json", auth="public", cors="*")
|
||||
def mail_chat_post(self, uuid, message_content, **kwargs):
|
||||
mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
if not mail_channel:
|
||||
return False
|
||||
|
||||
# find the author from the user session
|
||||
if request.session.uid:
|
||||
author = request.env['res.users'].sudo().browse(request.session.uid).partner_id
|
||||
author_id = author.id
|
||||
email_from = author.email_formatted
|
||||
else: # If Public User, use catchall email from company
|
||||
author_id = False
|
||||
email_from = mail_channel.anonymous_name or mail_channel.create_uid.company_id.catchall_formatted
|
||||
# post a message without adding followers to the channel. email_from=False avoid to get author from email data
|
||||
body = tools.plaintext2html(message_content)
|
||||
message = mail_channel.with_context(mail_create_nosubscribe=True).message_post(
|
||||
author_id=author_id,
|
||||
email_from=email_from,
|
||||
body=body,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment'
|
||||
)
|
||||
return message.id if message else False
|
||||
|
||||
@route(['/mail/chat_history'], type="json", auth="public", cors="*")
|
||||
def mail_chat_history(self, uuid, last_id=False, limit=20):
|
||||
channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
if not channel:
|
||||
return []
|
||||
else:
|
||||
return channel._channel_fetch_message(last_id, limit)
|
||||
609
odoo-bringout-oca-ocb-mail/mail/controllers/discuss.py
Normal file
609
odoo-bringout-oca-ocb-mail/mail/controllers/discuss.py
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from psycopg2 import IntegrityError
|
||||
from psycopg2.errorcodes import UNIQUE_VIOLATION
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.http import request
|
||||
from odoo.tools import consteq, file_open
|
||||
from odoo.tools.misc import get_lang
|
||||
from odoo.tools.translate import _
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
|
||||
class DiscussController(http.Controller):
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Public Pages
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route([
|
||||
'/chat/<string:create_token>',
|
||||
'/chat/<string:create_token>/<string:channel_name>',
|
||||
], methods=['GET'], type='http', auth='public')
|
||||
def discuss_channel_chat_from_token(self, create_token, channel_name=None, **kwargs):
|
||||
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')
|
||||
def discuss_channel_meet_from_token(self, create_token, channel_name=None, **kwargs):
|
||||
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')
|
||||
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs):
|
||||
channel_sudo = request.env['mail.channel'].browse(channel_id).sudo().exists()
|
||||
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token):
|
||||
raise NotFound()
|
||||
return self._response_discuss_channel_invitation(channel_sudo=channel_sudo)
|
||||
|
||||
@http.route('/discuss/channel/<int:channel_id>', methods=['GET'], type='http', auth='public')
|
||||
def discuss_channel(self, channel_id, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
return self._response_discuss_public_channel_template(channel_sudo=channel_member_sudo.channel_id)
|
||||
|
||||
def _response_discuss_channel_from_token(self, create_token, channel_name=None, default_display_mode=False):
|
||||
if not request.env['ir.config_parameter'].sudo().get_param('mail.chat_from_token'):
|
||||
raise NotFound()
|
||||
channel_sudo = request.env['mail.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=channel_sudo, is_channel_token_secret=False)
|
||||
|
||||
def _response_discuss_channel_invitation(self, channel_sudo, is_channel_token_secret=True):
|
||||
if channel_sudo.channel_type == 'chat':
|
||||
raise NotFound()
|
||||
discuss_public_view_data = {
|
||||
'isChannelTokenSecret': is_channel_token_secret,
|
||||
}
|
||||
add_guest_cookie = False
|
||||
channel_member_sudo = channel_sudo.env['mail.channel.member']._get_as_sudo_from_request(request=request, channel_id=channel_sudo.id)
|
||||
if channel_member_sudo:
|
||||
channel_sudo = channel_member_sudo.channel_id # ensure guest is in context
|
||||
else:
|
||||
if not channel_sudo.env.user._is_public():
|
||||
try:
|
||||
channel_sudo.add_members([channel_sudo.env.user.partner_id.id])
|
||||
except UserError:
|
||||
raise NotFound()
|
||||
else:
|
||||
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
|
||||
if guest:
|
||||
channel_sudo = channel_sudo.with_context(guest=guest)
|
||||
try:
|
||||
channel_sudo.add_members(guest_ids=[guest.id])
|
||||
except UserError:
|
||||
raise NotFound()
|
||||
else:
|
||||
if channel_sudo.group_public_id:
|
||||
raise NotFound()
|
||||
guest = channel_sudo.env['mail.guest'].create({
|
||||
'country_id': channel_sudo.env['res.country'].search([('code', '=', request.geoip.get('country_code'))], limit=1).id,
|
||||
'lang': get_lang(channel_sudo.env).code,
|
||||
'name': _("Guest"),
|
||||
'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request),
|
||||
})
|
||||
add_guest_cookie = True
|
||||
discuss_public_view_data.update({
|
||||
'shouldAddGuestAsMemberOnJoin': True,
|
||||
'shouldDisplayWelcomeViewInitially': True,
|
||||
})
|
||||
channel_sudo = channel_sudo.with_context(guest=guest)
|
||||
response = self._response_discuss_public_channel_template(channel_sudo=channel_sudo, discuss_public_view_data=discuss_public_view_data)
|
||||
if add_guest_cookie:
|
||||
# Discuss Guest ID: every route in this file will make use of it to authenticate
|
||||
# the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`.
|
||||
expiration_date = datetime.now() + timedelta(days=365)
|
||||
response.set_cookie(guest._cookie_name, f"{guest.id}{guest._cookie_separator}{guest.access_token}", httponly=True, expires=expiration_date)
|
||||
return response
|
||||
|
||||
def _response_discuss_public_channel_template(self, channel_sudo, 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_sudo.channel_info()[0],
|
||||
'discussPublicViewData': dict({
|
||||
'channel': [('insert', {'id': channel_sudo.id, 'model': 'mail.channel'})],
|
||||
'shouldDisplayWelcomeViewInitially': channel_sudo.default_display_mode == 'video_full_screen',
|
||||
}, **discuss_public_view_data),
|
||||
},
|
||||
'session_info': channel_sudo.env['ir.http'].session_info(),
|
||||
})
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Semi-Static Content (GET requests with possible cache)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/channel/<int:channel_id>/partner/<int:partner_id>/avatar_128', methods=['GET'], type='http', auth='public')
|
||||
def mail_channel_partner_avatar_128(self, channel_id, partner_id, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request(request=request, channel_id=channel_id)
|
||||
partner_sudo = channel_member_sudo.env['res.partner'].browse(partner_id).exists()
|
||||
placeholder = partner_sudo._avatar_get_placeholder_path()
|
||||
if channel_member_sudo and channel_member_sudo.env['mail.channel.member'].search([('channel_id', '=', channel_id), ('partner_id', '=', partner_id)], limit=1):
|
||||
return request.env['ir.binary']._get_image_stream_from(partner_sudo, field_name='avatar_128', placeholder=placeholder).get_response()
|
||||
if request.env.user.share:
|
||||
return request.env['ir.binary']._get_placeholder_stream(placeholder).get_response()
|
||||
return request.env['ir.binary']._get_image_stream_from(partner_sudo.sudo(False), field_name='avatar_128', placeholder=placeholder).get_response()
|
||||
|
||||
@http.route('/mail/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128', methods=['GET'], type='http', auth='public')
|
||||
def mail_channel_guest_avatar_128(self, channel_id, guest_id, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request(request=request, channel_id=channel_id)
|
||||
guest_sudo = channel_member_sudo.env['mail.guest'].browse(guest_id).exists()
|
||||
placeholder = guest_sudo._avatar_get_placeholder_path()
|
||||
if channel_member_sudo and channel_member_sudo.env['mail.channel.member'].search([('channel_id', '=', channel_id), ('guest_id', '=', guest_id)], limit=1):
|
||||
return request.env['ir.binary']._get_image_stream_from(guest_sudo, field_name='avatar_128', placeholder=placeholder).get_response()
|
||||
if request.env.user.share:
|
||||
return request.env['ir.binary']._get_placeholder_stream(placeholder).get_response()
|
||||
return request.env['ir.binary']._get_image_stream_from(guest_sudo.sudo(False), field_name='avatar_128', placeholder=placeholder).get_response()
|
||||
|
||||
@http.route('/mail/channel/<int:channel_id>/attachment/<int:attachment_id>', methods=['GET'], type='http', auth='public')
|
||||
def mail_channel_attachment(self, channel_id, attachment_id, download=None, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
attachment_sudo = channel_member_sudo.env['ir.attachment'].search([
|
||||
('id', '=', int(attachment_id)),
|
||||
('res_id', '=', int(channel_id)),
|
||||
('res_model', '=', 'mail.channel')
|
||||
], limit=1)
|
||||
if not attachment_sudo:
|
||||
raise NotFound()
|
||||
return request.env['ir.binary']._get_stream_from(attachment_sudo).get_response(as_attachment=download)
|
||||
|
||||
@http.route([
|
||||
'/mail/channel/<int:channel_id>/image/<int:attachment_id>',
|
||||
'/mail/channel/<int:channel_id>/image/<int:attachment_id>/<int:width>x<int:height>',
|
||||
], methods=['GET'], type='http', auth='public')
|
||||
def fetch_image(self, channel_id, attachment_id, width=0, height=0, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
attachment_sudo = channel_member_sudo.env['ir.attachment'].search([
|
||||
('id', '=', int(attachment_id)),
|
||||
('res_id', '=', int(channel_id)),
|
||||
('res_model', '=', 'mail.channel'),
|
||||
], 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'))
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Client Initialization
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/init_messaging', methods=['POST'], type='json', auth='public')
|
||||
def mail_init_messaging(self, **kwargs):
|
||||
if not request.env.user.sudo()._is_public():
|
||||
return request.env.user.sudo(request.env.user.has_group('base.group_portal'))._init_messaging()
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
if guest:
|
||||
return guest.sudo()._init_messaging()
|
||||
raise NotFound()
|
||||
|
||||
@http.route('/mail/load_message_failures', methods=['POST'], type='json', auth='user')
|
||||
def mail_load_message_failures(self, **kwargs):
|
||||
return request.env.user.partner_id._message_fetch_failed()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Mailbox
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/inbox/messages', methods=['POST'], type='json', auth='user')
|
||||
def discuss_inbox_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
||||
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], max_id=max_id, min_id=min_id, limit=limit).message_format()
|
||||
|
||||
@http.route('/mail/history/messages', methods=['POST'], type='json', auth='user')
|
||||
def discuss_history_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
||||
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], max_id=max_id, min_id=min_id, limit=limit).message_format()
|
||||
|
||||
@http.route('/mail/starred/messages', methods=['POST'], type='json', auth='user')
|
||||
def discuss_starred_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
|
||||
return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], max_id=max_id, min_id=min_id, limit=limit).message_format()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Thread API (channel/chatter common)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
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')
|
||||
def mail_message_post(self, thread_model, thread_id, post_data, **kwargs):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
guest.env['ir.attachment'].browse(post_data.get('attachment_ids', []))._check_attachments_access(post_data.get('attachment_tokens'))
|
||||
if thread_model == 'mail.channel':
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(thread_id))
|
||||
thread = channel_member_sudo.channel_id
|
||||
else:
|
||||
thread = request.env[thread_model].browse(int(thread_id)).exists()
|
||||
return thread.message_post(**{key: value for key, value in post_data.items() if key in self._get_allowed_message_post_params()}).message_format()[0]
|
||||
|
||||
@http.route('/mail/message/update_content', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_update_content(self, message_id, body, attachment_ids, attachment_tokens=None, **kwargs):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
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()
|
||||
guest.env[message_sudo.model].browse([message_sudo.res_id])._message_update_content(
|
||||
message_sudo,
|
||||
body,
|
||||
attachment_ids=attachment_ids
|
||||
)
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'body': message_sudo.body,
|
||||
'attachments': message_sudo.attachment_ids.sorted()._attachment_format(),
|
||||
}
|
||||
|
||||
@http.route('/mail/attachment/upload', methods=['POST'], type='http', auth='public')
|
||||
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
||||
channel_member = request.env['mail.channel.member']
|
||||
if thread_model == 'mail.channel':
|
||||
channel_member = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(thread_id))
|
||||
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 channel_member.env.user.share:
|
||||
# Only generate the access token if absolutely necessary (= not for internal user).
|
||||
vals['access_token'] = channel_member.env['ir.attachment']._generate_access_token()
|
||||
try:
|
||||
attachment = channel_member.env['ir.attachment'].create(vals)
|
||||
attachment._post_add_create()
|
||||
attachmentData = {
|
||||
'filename': ufile.filename,
|
||||
'id': attachment.id,
|
||||
'mimetype': attachment.mimetype,
|
||||
'name': attachment.name,
|
||||
'size': attachment.file_size
|
||||
}
|
||||
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')
|
||||
def mail_attachment_delete(self, attachment_id, access_token=None, **kwargs):
|
||||
attachment_sudo = request.env['ir.attachment'].browse(int(attachment_id)).sudo().exists()
|
||||
if not attachment_sudo:
|
||||
target = request.env.user.partner_id
|
||||
request.env['bus.bus']._sendone(target, 'ir.attachment/delete', {'id': attachment_id})
|
||||
return
|
||||
if not request.env.user.share:
|
||||
# Check through standard access rights/rules for internal users.
|
||||
attachment_sudo.sudo(False)._delete_and_notify()
|
||||
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).
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
message_sudo = guest.env['mail.message'].sudo().search([('attachment_ids', 'in', attachment_sudo.ids)], limit=1)
|
||||
if message_sudo:
|
||||
if not message_sudo.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()
|
||||
|
||||
@http.route('/mail/message/add_reaction', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_add_reaction(self, message_id, content):
|
||||
guest_sudo = request.env['mail.guest']._get_guest_from_request(request).sudo()
|
||||
message_sudo = guest_sudo.env['mail.message'].browse(int(message_id)).exists()
|
||||
if not message_sudo:
|
||||
raise NotFound()
|
||||
if request.env.user.sudo()._is_public():
|
||||
if not guest_sudo or not message_sudo.model == 'mail.channel' or message_sudo.res_id not in guest_sudo.channel_ids.ids:
|
||||
raise NotFound()
|
||||
message_sudo._message_add_reaction(content=content)
|
||||
guests = [('insert', {'id': guest_sudo.id})]
|
||||
partners = []
|
||||
else:
|
||||
message_sudo.sudo(False)._message_add_reaction(content=content)
|
||||
guests = []
|
||||
partners = [('insert', {'id': request.env.user.partner_id.id})]
|
||||
reactions = message_sudo.env['mail.message.reaction'].search([('message_id', '=', message_sudo.id), ('content', '=', content)])
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'message': {'id', message_sudo.id},
|
||||
'partners': partners,
|
||||
})],
|
||||
}
|
||||
|
||||
@http.route('/mail/message/remove_reaction', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_remove_reaction(self, message_id, content):
|
||||
guest_sudo = request.env['mail.guest']._get_guest_from_request(request).sudo()
|
||||
message_sudo = guest_sudo.env['mail.message'].browse(int(message_id)).exists()
|
||||
if not message_sudo:
|
||||
raise NotFound()
|
||||
if request.env.user.sudo()._is_public():
|
||||
if not guest_sudo or not message_sudo.model == 'mail.channel' or message_sudo.res_id not in guest_sudo.channel_ids.ids:
|
||||
raise NotFound()
|
||||
message_sudo._message_remove_reaction(content=content)
|
||||
guests = [('insert-and-unlink', {'id': guest_sudo.id})]
|
||||
partners = []
|
||||
else:
|
||||
message_sudo.sudo(False)._message_remove_reaction(content=content)
|
||||
guests = []
|
||||
partners = [('insert-and-unlink', {'id': request.env.user.partner_id.id})]
|
||||
reactions = message_sudo.env['mail.message.reaction'].search([('message_id', '=', message_sudo.id), ('content', '=', content)])
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'message': {'id': message_sudo.id},
|
||||
'partners': partners,
|
||||
})],
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Channel API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/channel/add_guest_as_member', methods=['POST'], type='json', auth='public')
|
||||
def mail_channel_add_guest_as_member(self, channel_id, channel_uuid, **kwargs):
|
||||
channel_sudo = request.env['mail.channel'].browse(int(channel_id)).sudo().exists()
|
||||
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, channel_uuid):
|
||||
raise NotFound()
|
||||
if channel_sudo.channel_type == 'chat':
|
||||
raise NotFound()
|
||||
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
|
||||
# Only guests should take this route.
|
||||
if not guest:
|
||||
raise NotFound()
|
||||
channel_member = channel_sudo.env['mail.channel.member']._get_as_sudo_from_request(request=request, channel_id=channel_id)
|
||||
# Do not add the guest to channel members if they are already member.
|
||||
if not channel_member:
|
||||
channel_sudo = channel_sudo.with_context(guest=guest)
|
||||
try:
|
||||
channel_sudo.add_members(guest_ids=[guest.id])
|
||||
except UserError:
|
||||
raise NotFound()
|
||||
|
||||
@http.route('/mail/channel/messages', methods=['POST'], type='json', auth='public')
|
||||
def mail_channel_messages(self, channel_id, max_id=None, min_id=None, limit=30, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
messages = channel_member_sudo.env['mail.message']._message_fetch(domain=[
|
||||
('res_id', '=', channel_id),
|
||||
('model', '=', 'mail.channel'),
|
||||
('message_type', '!=', 'user_notification'),
|
||||
], max_id=max_id, min_id=min_id, limit=limit)
|
||||
if not request.env.user._is_public():
|
||||
messages.set_message_done()
|
||||
return messages.message_format()
|
||||
|
||||
@http.route('/mail/channel/set_last_seen_message', methods=['POST'], type='json', auth='public')
|
||||
def mail_channel_mark_as_seen(self, channel_id, last_message_id, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
return channel_member_sudo.channel_id._channel_seen(int(last_message_id))
|
||||
|
||||
@http.route('/mail/channel/notify_typing', methods=['POST'], type='json', auth='public')
|
||||
def mail_channel_notify_typing(self, channel_id, is_typing, **kwargs):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
channel_member_sudo._notify_typing(is_typing)
|
||||
|
||||
@http.route('/mail/channel/ping', methods=['POST'], type='json', auth='public')
|
||||
def channel_ping(self, channel_id, rtc_session_id=None, check_rtc_session_ids=None):
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
if rtc_session_id:
|
||||
channel_member_sudo.channel_id.rtc_session_ids.filtered_domain([
|
||||
('id', '=', int(rtc_session_id)),
|
||||
('channel_member_id', '=', channel_member_sudo.id),
|
||||
]).write({}) # update write_date
|
||||
current_rtc_sessions, outdated_rtc_sessions = channel_member_sudo._rtc_sync_sessions(check_rtc_session_ids=check_rtc_session_ids)
|
||||
return {'rtcSessions': [
|
||||
('insert', [rtc_session_sudo._mail_rtc_session_format() for rtc_session_sudo in current_rtc_sessions]),
|
||||
('insert-and-unlink', [{'id': missing_rtc_session_sudo.id} for missing_rtc_session_sudo in outdated_rtc_sessions]),
|
||||
]}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Chatter API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/thread/data', methods=['POST'], type='json', auth='user')
|
||||
def mail_thread_data(self, thread_model, thread_id, request_list, **kwargs):
|
||||
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, max_id=None, min_id=None, limit=30, **kwargs):
|
||||
messages = request.env['mail.message']._message_fetch(domain=[
|
||||
('res_id', '=', int(thread_id)),
|
||||
('model', '=', thread_model),
|
||||
('message_type', '!=', 'user_notification'),
|
||||
], max_id=max_id, min_id=min_id, limit=limit)
|
||||
if not request.env.user._is_public():
|
||||
messages.set_message_done()
|
||||
return messages.message_format()
|
||||
|
||||
@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']))
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# RTC API TODO move check logic in routes.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/rtc/session/notify_call_members', methods=['POST'], type="json", auth="public")
|
||||
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_request(request)
|
||||
notifications_by_session = defaultdict(list)
|
||||
for sender_session_id, target_session_ids, content in peer_notifications:
|
||||
session_sudo = guest.env['mail.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")
|
||||
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_request(request)
|
||||
if guest:
|
||||
session = guest.env['mail.channel.rtc.session'].sudo().browse(int(session_id)).exists()
|
||||
if session and session.guest_id == guest:
|
||||
session._update_and_broadcast(values)
|
||||
return
|
||||
return
|
||||
session = request.env['mail.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")
|
||||
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_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
return channel_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")
|
||||
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
|
||||
"""
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
return channel_member_sudo._rtc_leave_call()
|
||||
|
||||
@http.route('/mail/rtc/channel/cancel_call_invitation', methods=['POST'], type="json", auth="public")
|
||||
def channel_call_cancel_invitation(self, channel_id, member_ids=None):
|
||||
""" Sends invitations to join the RTC call to all connected members of the thread who are not already invited,
|
||||
if member_ids is provided, only the specified ids will be invited.
|
||||
|
||||
:param list member_ids: list of member ids to invite
|
||||
"""
|
||||
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
|
||||
return channel_member_sudo.channel_id._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', 'max-age=%s' % http.STATIC_CACHE),
|
||||
]
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Guest API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/guest/update_name', methods=['POST'], type='json', auth='public')
|
||||
def mail_guest_update_name(self, guest_id, name):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
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)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Link preview API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/link_preview', methods=['POST'], type='json', auth='public')
|
||||
def mail_link_preview(self, message_id):
|
||||
if not request.env['mail.link.preview'].sudo()._is_link_preview_enabled():
|
||||
return
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
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
|
||||
guest.env['mail.link.preview'].sudo()._create_link_previews(message)
|
||||
|
||||
@http.route('/mail/link_preview/delete', methods=['POST'], type='json', auth='public')
|
||||
def mail_link_preview_delete(self, link_preview_id):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
link_preview_sudo = guest.env['mail.link.preview'].sudo().search([('id', '=', int(link_preview_id))])
|
||||
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._delete_and_notify()
|
||||
42
odoo-bringout-oca-ocb-mail/mail/controllers/home.py
Normal file
42
odoo-bringout-oca-ocb-mail/mail/controllers/home.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import ipaddress
|
||||
|
||||
from odoo import _, SUPERUSER_ID
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.home import Home as WebHome
|
||||
|
||||
def _admin_password_warn(uid):
|
||||
""" Admin still has `admin` password, flash a message via chatter.
|
||||
|
||||
Uses a private mail.channel from the system (/ odoobot) to the user, as
|
||||
using a more generic mail.thread could send an email which is undesirable
|
||||
|
||||
Uses mail.channel directly because using mail.thread might send an email instead.
|
||||
"""
|
||||
if request.params['password'] != 'admin':
|
||||
return
|
||||
if ipaddress.ip_address(request.httprequest.remote_addr).is_private:
|
||||
return
|
||||
env = request.env(user=SUPERUSER_ID, su=True)
|
||||
admin = env.ref('base.partner_admin')
|
||||
if uid not in admin.user_ids.ids:
|
||||
return
|
||||
has_demo = bool(env['ir.module.module'].search_count([('demo', '=', True)]))
|
||||
if has_demo:
|
||||
return
|
||||
|
||||
user = request.env(user=uid)['res.users']
|
||||
MailChannel = env(context=user.context_get())['mail.channel']
|
||||
MailChannel.browse(MailChannel.channel_get([admin.id])['id'])\
|
||||
.message_post(
|
||||
body=_("Your password is the default (admin)! If this system is exposed to untrusted users it is important to change it immediately for security reasons. I will keep nagging you about it!"),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment'
|
||||
)
|
||||
|
||||
class Home(WebHome):
|
||||
def _login_redirect(self, uid, redirect=None):
|
||||
if request.params.get('login_success'):
|
||||
_admin_password_warn(uid)
|
||||
|
||||
return super()._login_redirect(uid, redirect)
|
||||
206
odoo-bringout-oca-ocb-mail/mail/controllers/mail.py
Normal file
206
odoo-bringout-oca-ocb-mail/mail/controllers/mail.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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_generic_fallback(cls, model, res_id, access_token=None, **kwargs):
|
||||
if request.session.uid is None:
|
||||
return cls._redirect_to_login_with_mail_view(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
@classmethod
|
||||
def _redirect_to_messaging(cls):
|
||||
url = '/web#%s' % url_encode({'action': 'mail.action_discuss'})
|
||||
return request.redirect(url)
|
||||
|
||||
@classmethod
|
||||
def _redirect_to_login_with_mail_view(cls, model, res_id, access_token=None, **kwargs):
|
||||
url_base = '/mail/view'
|
||||
url_params = request.env['mail.thread']._notify_get_action_link_params(
|
||||
'view', **{
|
||||
'model': model,
|
||||
'res_id': res_id,
|
||||
'access_token': access_token,
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
mail_view_url = f'{url_base}?{url_encode(url_params, sort=True)}'
|
||||
return request.redirect(f'/web/login?{url_encode({"redirect": mail_view_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']._notify_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_generic_fallback(model, res_id)
|
||||
try:
|
||||
record = request.env[model].browse(res_id).exists()
|
||||
except Exception:
|
||||
record = None
|
||||
redirect = cls._redirect_to_generic_fallback(model, res_id)
|
||||
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_generic_fallback(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
|
||||
# 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_generic_fallback(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
|
||||
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_generic_fallback(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
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_generic_fallback(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
else:
|
||||
record_action = record_sudo._get_access_action(access_uid=uid)
|
||||
else:
|
||||
record_action = record_sudo._get_access_action()
|
||||
# we have an act_url (probably a portal link): we need to retry being logged to check access
|
||||
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
|
||||
return cls._redirect_to_login_with_mail_view(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
|
||||
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'])
|
||||
# anything else than an act_window is not supported
|
||||
elif not record_action['type'] == 'ir.actions.act_window':
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
# backend act_window: when not logged, unless really readable as public,
|
||||
# user is going to be redirected to login -> keep mail/view as redirect
|
||||
# in that case. In case of readable record, we consider this might be
|
||||
# a customization and we do not change the behavior in stable
|
||||
if uid is None or request.env.user._is_public():
|
||||
has_access = record_sudo.with_user(request.env.user).check_access_rights('read', raise_exception=False)
|
||||
if has_access:
|
||||
try:
|
||||
record_sudo.with_user(request.env.user).check_access_rule('read')
|
||||
except AccessError:
|
||||
has_access = False
|
||||
if not has_access:
|
||||
return cls._redirect_to_login_with_mail_view(
|
||||
model, res_id, access_token=access_token, **kwargs,
|
||||
)
|
||||
|
||||
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, sort=True)
|
||||
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)
|
||||
69
odoo-bringout-oca-ocb-mail/mail/data/ir_cron_data.xml
Normal file
69
odoo-bringout-oca-ocb-mail/mail/data/ir_cron_data.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?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>
|
||||
</data>
|
||||
</odoo>
|
||||
43
odoo-bringout-oca-ocb-mail/mail/data/mail_activity_data.xml
Normal file
43
odoo-bringout-oca-ocb-mail/mail/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>
|
||||
32
odoo-bringout-oca-ocb-mail/mail/data/mail_channel_data.xml
Normal file
32
odoo-bringout-oca-ocb-mail/mail/data/mail_channel_data.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="mail.channel" id="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="module_install_notification">
|
||||
<field name="model">mail.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="mail.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="mail.channel" id="mail.channel_all_employees">
|
||||
<field name="group_ids" eval="[Command.link(ref('base.group_user'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
159
odoo-bringout-oca-ocb-mail/mail/data/mail_channel_demo.xml
Normal file
159
odoo-bringout-oca-ocb-mail/mail/data/mail_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="mail.channel" id="channel_1">
|
||||
<field name="name">sales</field>
|
||||
<field name="description">Discussion about best sales practices and deals.</field>
|
||||
</record>
|
||||
<record model="mail.channel" id="channel_2">
|
||||
<field name="name">board-meetings</field>
|
||||
<field name="description">Board meetings, budgets, strategic plans</field>
|
||||
</record>
|
||||
<record model="mail.channel" id="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">mail.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="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">mail.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="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">mail.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="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">mail.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">mail.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">mail.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">mail.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">mail.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="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>
|
||||
18
odoo-bringout-oca-ocb-mail/mail/data/mail_groups.xml
Normal file
18
odoo-bringout-oca-ocb-mail/mail/data/mail_groups.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?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>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<?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">
|
||||
<img t-att-src="'/logo.png?company=%s' % (company.id or 0)" 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.secondary_color if company.secondary_color else '#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="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>
|
||||
<td>&nbsp;&nbsp;</td>
|
||||
|
||||
<td t-if="actions">
|
||||
<t t-foreach="actions" t-as="action">
|
||||
|
|
||||
<a t-att-href="action['url']" style="font-size: 12px; color: #875A7B; text-decoration:none !important;">
|
||||
<t t-out="action['title']"/>
|
||||
</a>
|
||||
</t>
|
||||
</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>
|
||||
<p style="color: #555555; font-size:11px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=email" style="color: #875A7B;">Odoo</a>
|
||||
</p>
|
||||
</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">
|
||||
<img t-att-src="'/logo.png?company=%s' % (company.id or 0)" 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" style="color: #875A7B;">Odoo</a>
|
||||
</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>
|
||||
|
|
@ -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>
|
||||
6
odoo-bringout-oca-ocb-mail/mail/data/neutralize.sql
Normal file
6
odoo-bringout-oca-ocb-mail/mail/data/neutralize.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- deactivate mail template
|
||||
UPDATE mail_template
|
||||
SET mail_server_id = NULL;
|
||||
-- deactivate fetchmail server
|
||||
UPDATE fetchmail_server
|
||||
SET active = false;
|
||||
14
odoo-bringout-oca-ocb-mail/mail/data/res_partner_data.xml
Normal file
14
odoo-bringout-oca-ocb-mail/mail/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>
|
||||
35813
odoo-bringout-oca-ocb-mail/mail/i18n/af.po
Normal file
35813
odoo-bringout-oca-ocb-mail/mail/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
36016
odoo-bringout-oca-ocb-mail/mail/i18n/am.po
Normal file
36016
odoo-bringout-oca-ocb-mail/mail/i18n/am.po
Normal file
File diff suppressed because it is too large
Load diff
36354
odoo-bringout-oca-ocb-mail/mail/i18n/ar.po
Normal file
36354
odoo-bringout-oca-ocb-mail/mail/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
36110
odoo-bringout-oca-ocb-mail/mail/i18n/az.po
Normal file
36110
odoo-bringout-oca-ocb-mail/mail/i18n/az.po
Normal file
File diff suppressed because it is too large
Load diff
36036
odoo-bringout-oca-ocb-mail/mail/i18n/be.po
Normal file
36036
odoo-bringout-oca-ocb-mail/mail/i18n/be.po
Normal file
File diff suppressed because it is too large
Load diff
36242
odoo-bringout-oca-ocb-mail/mail/i18n/bg.po
Normal file
36242
odoo-bringout-oca-ocb-mail/mail/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load diff
36029
odoo-bringout-oca-ocb-mail/mail/i18n/bs.po
Normal file
36029
odoo-bringout-oca-ocb-mail/mail/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
36471
odoo-bringout-oca-ocb-mail/mail/i18n/ca.po
Normal file
36471
odoo-bringout-oca-ocb-mail/mail/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load diff
36332
odoo-bringout-oca-ocb-mail/mail/i18n/cs.po
Normal file
36332
odoo-bringout-oca-ocb-mail/mail/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load diff
36271
odoo-bringout-oca-ocb-mail/mail/i18n/da.po
Normal file
36271
odoo-bringout-oca-ocb-mail/mail/i18n/da.po
Normal file
File diff suppressed because it is too large
Load diff
36491
odoo-bringout-oca-ocb-mail/mail/i18n/de.po
Normal file
36491
odoo-bringout-oca-ocb-mail/mail/i18n/de.po
Normal file
File diff suppressed because it is too large
Load diff
6296
odoo-bringout-oca-ocb-mail/mail/i18n/el.po
Normal file
6296
odoo-bringout-oca-ocb-mail/mail/i18n/el.po
Normal file
File diff suppressed because it is too large
Load diff
5664
odoo-bringout-oca-ocb-mail/mail/i18n/en_GB.po
Normal file
5664
odoo-bringout-oca-ocb-mail/mail/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load diff
36485
odoo-bringout-oca-ocb-mail/mail/i18n/es.po
Normal file
36485
odoo-bringout-oca-ocb-mail/mail/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_BO.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CL.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CO.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CR.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_DO.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_EC.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load diff
36478
odoo-bringout-oca-ocb-mail/mail/i18n/es_MX.po
Normal file
36478
odoo-bringout-oca-ocb-mail/mail/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_PE.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_PY.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_VE.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load diff
36102
odoo-bringout-oca-ocb-mail/mail/i18n/et.po
Normal file
36102
odoo-bringout-oca-ocb-mail/mail/i18n/et.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/eu.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load diff
36600
odoo-bringout-oca-ocb-mail/mail/i18n/fa.po
Normal file
36600
odoo-bringout-oca-ocb-mail/mail/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load diff
36217
odoo-bringout-oca-ocb-mail/mail/i18n/fi.po
Normal file
36217
odoo-bringout-oca-ocb-mail/mail/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/fo.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load diff
36488
odoo-bringout-oca-ocb-mail/mail/i18n/fr.po
Normal file
36488
odoo-bringout-oca-ocb-mail/mail/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/fr_CA.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/gl.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load diff
35840
odoo-bringout-oca-ocb-mail/mail/i18n/gu.po
Normal file
35840
odoo-bringout-oca-ocb-mail/mail/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load diff
36185
odoo-bringout-oca-ocb-mail/mail/i18n/he.po
Normal file
36185
odoo-bringout-oca-ocb-mail/mail/i18n/he.po
Normal file
File diff suppressed because it is too large
Load diff
36056
odoo-bringout-oca-ocb-mail/mail/i18n/hi.po
Normal file
36056
odoo-bringout-oca-ocb-mail/mail/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load diff
36218
odoo-bringout-oca-ocb-mail/mail/i18n/hr.po
Normal file
36218
odoo-bringout-oca-ocb-mail/mail/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load diff
36214
odoo-bringout-oca-ocb-mail/mail/i18n/hu.po
Normal file
36214
odoo-bringout-oca-ocb-mail/mail/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load diff
36016
odoo-bringout-oca-ocb-mail/mail/i18n/hy.po
Normal file
36016
odoo-bringout-oca-ocb-mail/mail/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load diff
36385
odoo-bringout-oca-ocb-mail/mail/i18n/id.po
Normal file
36385
odoo-bringout-oca-ocb-mail/mail/i18n/id.po
Normal file
File diff suppressed because it is too large
Load diff
35822
odoo-bringout-oca-ocb-mail/mail/i18n/is.po
Normal file
35822
odoo-bringout-oca-ocb-mail/mail/i18n/is.po
Normal file
File diff suppressed because it is too large
Load diff
36229
odoo-bringout-oca-ocb-mail/mail/i18n/it.po
Normal file
36229
odoo-bringout-oca-ocb-mail/mail/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
36157
odoo-bringout-oca-ocb-mail/mail/i18n/ja.po
Normal file
36157
odoo-bringout-oca-ocb-mail/mail/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/ka.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/ka.po
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/kab.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/kab.po
Normal file
File diff suppressed because it is too large
Load diff
36106
odoo-bringout-oca-ocb-mail/mail/i18n/km.po
Normal file
36106
odoo-bringout-oca-ocb-mail/mail/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
36365
odoo-bringout-oca-ocb-mail/mail/i18n/ko.po
Normal file
36365
odoo-bringout-oca-ocb-mail/mail/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load diff
6461
odoo-bringout-oca-ocb-mail/mail/i18n/lb.po
Normal file
6461
odoo-bringout-oca-ocb-mail/mail/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load diff
35856
odoo-bringout-oca-ocb-mail/mail/i18n/lo.po
Normal file
35856
odoo-bringout-oca-ocb-mail/mail/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load diff
36217
odoo-bringout-oca-ocb-mail/mail/i18n/lt.po
Normal file
36217
odoo-bringout-oca-ocb-mail/mail/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load diff
36288
odoo-bringout-oca-ocb-mail/mail/i18n/lv.po
Normal file
36288
odoo-bringout-oca-ocb-mail/mail/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load diff
36029
odoo-bringout-oca-ocb-mail/mail/i18n/mail.pot
Normal file
36029
odoo-bringout-oca-ocb-mail/mail/i18n/mail.pot
Normal file
File diff suppressed because it is too large
Load diff
5658
odoo-bringout-oca-ocb-mail/mail/i18n/mk.po
Normal file
5658
odoo-bringout-oca-ocb-mail/mail/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load diff
35819
odoo-bringout-oca-ocb-mail/mail/i18n/ml.po
Normal file
35819
odoo-bringout-oca-ocb-mail/mail/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
36194
odoo-bringout-oca-ocb-mail/mail/i18n/mn.po
Normal file
36194
odoo-bringout-oca-ocb-mail/mail/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load diff
35841
odoo-bringout-oca-ocb-mail/mail/i18n/ms.po
Normal file
35841
odoo-bringout-oca-ocb-mail/mail/i18n/ms.po
Normal file
File diff suppressed because it is too large
Load diff
36149
odoo-bringout-oca-ocb-mail/mail/i18n/nb.po
Normal file
36149
odoo-bringout-oca-ocb-mail/mail/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load diff
5655
odoo-bringout-oca-ocb-mail/mail/i18n/ne.po
Normal file
5655
odoo-bringout-oca-ocb-mail/mail/i18n/ne.po
Normal file
File diff suppressed because it is too large
Load diff
36446
odoo-bringout-oca-ocb-mail/mail/i18n/nl.po
Normal file
36446
odoo-bringout-oca-ocb-mail/mail/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
35813
odoo-bringout-oca-ocb-mail/mail/i18n/no.po
Normal file
35813
odoo-bringout-oca-ocb-mail/mail/i18n/no.po
Normal file
File diff suppressed because it is too large
Load diff
36219
odoo-bringout-oca-ocb-mail/mail/i18n/pl.po
Normal file
36219
odoo-bringout-oca-ocb-mail/mail/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load diff
36207
odoo-bringout-oca-ocb-mail/mail/i18n/pt.po
Normal file
36207
odoo-bringout-oca-ocb-mail/mail/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load diff
36208
odoo-bringout-oca-ocb-mail/mail/i18n/pt_BR.po
Normal file
36208
odoo-bringout-oca-ocb-mail/mail/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load diff
36439
odoo-bringout-oca-ocb-mail/mail/i18n/ro.po
Normal file
36439
odoo-bringout-oca-ocb-mail/mail/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load diff
36384
odoo-bringout-oca-ocb-mail/mail/i18n/ru.po
Normal file
36384
odoo-bringout-oca-ocb-mail/mail/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load diff
36221
odoo-bringout-oca-ocb-mail/mail/i18n/sk.po
Normal file
36221
odoo-bringout-oca-ocb-mail/mail/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load diff
36233
odoo-bringout-oca-ocb-mail/mail/i18n/sl.po
Normal file
36233
odoo-bringout-oca-ocb-mail/mail/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load diff
36016
odoo-bringout-oca-ocb-mail/mail/i18n/sq.po
Normal file
36016
odoo-bringout-oca-ocb-mail/mail/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load diff
36268
odoo-bringout-oca-ocb-mail/mail/i18n/sr.po
Normal file
36268
odoo-bringout-oca-ocb-mail/mail/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load diff
5661
odoo-bringout-oca-ocb-mail/mail/i18n/sr@latin.po
Normal file
5661
odoo-bringout-oca-ocb-mail/mail/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load diff
36437
odoo-bringout-oca-ocb-mail/mail/i18n/sv.po
Normal file
36437
odoo-bringout-oca-ocb-mail/mail/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load diff
36016
odoo-bringout-oca-ocb-mail/mail/i18n/sw.po
Normal file
36016
odoo-bringout-oca-ocb-mail/mail/i18n/sw.po
Normal file
File diff suppressed because it is too large
Load diff
36016
odoo-bringout-oca-ocb-mail/mail/i18n/ta.po
Normal file
36016
odoo-bringout-oca-ocb-mail/mail/i18n/ta.po
Normal file
File diff suppressed because it is too large
Load diff
36311
odoo-bringout-oca-ocb-mail/mail/i18n/th.po
Normal file
36311
odoo-bringout-oca-ocb-mail/mail/i18n/th.po
Normal file
File diff suppressed because it is too large
Load diff
36420
odoo-bringout-oca-ocb-mail/mail/i18n/tr.po
Normal file
36420
odoo-bringout-oca-ocb-mail/mail/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load diff
36196
odoo-bringout-oca-ocb-mail/mail/i18n/uk.po
Normal file
36196
odoo-bringout-oca-ocb-mail/mail/i18n/uk.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…
Add table
Add a link
Reference in a new issue