11 changed files with 2099 additions and 0 deletions
@ -0,0 +1,22 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# Copyright (C) 2009-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
||||
|
# Author: Nilmar Shereef(<http://www.cybrosys.com>) |
||||
|
# you can modify it under the terms of the GNU LESSER |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
||||
|
# |
||||
|
# It is forbidden to publish, distribute, sublicense, or sell copies |
||||
|
# of the Software or modified copies of the Software. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
@ -0,0 +1,41 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# Copyright (C) 2009-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
||||
|
# Author: Nilmar Shereef(<http://www.cybrosys.com>) |
||||
|
# you can modify it under the terms of the GNU LESSER |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
||||
|
# |
||||
|
# It is forbidden to publish, distribute, sublicense, or sell copies |
||||
|
# of the Software or modified copies of the Software. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
{ |
||||
|
'name': 'Sent Mails', |
||||
|
'version': '1.0', |
||||
|
'author': 'Cybrosys Techno Solutions', |
||||
|
'company': 'Cybrosys Techno Solutions', |
||||
|
'website': 'http://www.cybrosys.com', |
||||
|
"category": "Discuss", |
||||
|
'depends': ['base', 'mail'], |
||||
|
'license': 'AGPL-3', |
||||
|
'data': [ |
||||
|
'views/get_sent_mails.xml', |
||||
|
], |
||||
|
'qweb': [ |
||||
|
'static/src/xml/client_action_sent_mails.xml', |
||||
|
|
||||
|
], |
||||
|
'installable': True, |
||||
|
'auto_install': False |
||||
|
} |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,44 @@ |
|||||
|
<section class="oe_container"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<h2 class="oe_slogan">Show sent mails</h2> |
||||
|
<h3 class="oe_slogan">Shows the mails or discussions sent by current user</h3> |
||||
|
<h4 class="oe_slogan">Author : Cybrosys Techno Solutions, www.cybrosys.com</h4> |
||||
|
</div> |
||||
|
|
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<p class='oe_mt32'> |
||||
|
☛This module enables the feature to display the mails and discussions done by current user |
||||
|
</p> |
||||
|
<div class="oe_centeralign oe_websiteonly"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row"> |
||||
|
<h2 class="oe_slogan">Sent Mail Menu Under Discuss</h2> |
||||
|
<div class="oe_span6"> |
||||
|
<div class="oe_row_img oe_centered"> |
||||
|
<img class="oe_picture oe_screenshot" src="sent_mails_demo.png"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<h2 class="oe_slogan" style="margin-top:20px;" >Need Any Help?</h2> |
||||
|
<div class="oe_slogan" style="margin-top:10px !important;"> |
||||
|
<a class="btn btn-primary btn-lg mt8" |
||||
|
style="color: #FFFFFF !important;" href="http://www.cybrosys.com"><i |
||||
|
class="fa fa-envelope"></i> Email </a> <a |
||||
|
class="btn btn-primary btn-lg mt8" style="color: #FFFFFF !important;" |
||||
|
href="http://www.cybrosys.com/contact/"><i |
||||
|
class="fa fa-phone"></i> Contact Us </a> <a |
||||
|
class="btn btn-primary btn-lg mt8" style="color: #FFFFFF !important;" |
||||
|
href="http://www.cybrosys.com/odoo-customization-and-installation/"><i |
||||
|
class="fa fa-check-square"></i> Request Customization </a> |
||||
|
</div> |
||||
|
<img src="cybro_logo.png" style="width: 190px; margin-bottom: 20px;" class="center-block"> |
||||
|
</section> |
After Width: | Height: | Size: 52 KiB |
@ -0,0 +1,180 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details. |
||||
|
""" |
||||
|
openerp_mailgate.py |
||||
|
""" |
||||
|
|
||||
|
import cgitb |
||||
|
import time |
||||
|
import optparse |
||||
|
import sys |
||||
|
import xmlrpclib |
||||
|
import smtplib |
||||
|
from email.mime.multipart import MIMEMultipart |
||||
|
from email.mime.base import MIMEBase |
||||
|
from email.mime.text import MIMEText |
||||
|
from email.utils import COMMASPACE, formatdate |
||||
|
from email import Encoders |
||||
|
|
||||
|
class DefaultConfig(object): |
||||
|
""" |
||||
|
Default configuration |
||||
|
""" |
||||
|
OPENERP_DEFAULT_USER_ID = 1 |
||||
|
OPENERP_DEFAULT_PASSWORD = 'admin' |
||||
|
OPENERP_HOSTNAME = 'localhost' |
||||
|
OPENERP_PORT = 8069 |
||||
|
OPENERP_DEFAULT_DATABASE = 'openerp' |
||||
|
MAIL_ERROR = 'error@example.com' |
||||
|
MAIL_SERVER = 'smtp.example.com' |
||||
|
MAIL_SERVER_PORT = 25 |
||||
|
MAIL_ADMINS = ('info@example.com',) |
||||
|
|
||||
|
config = DefaultConfig() |
||||
|
|
||||
|
|
||||
|
def send_mail(_from_, to_, subject, text, files=None, server=config.MAIL_SERVER, port=config.MAIL_SERVER_PORT): |
||||
|
assert isinstance(to_, (list, tuple)) |
||||
|
|
||||
|
if files is None: |
||||
|
files = [] |
||||
|
|
||||
|
msg = MIMEMultipart() |
||||
|
msg['From'] = _from_ |
||||
|
msg['To'] = COMMASPACE.join(to_) |
||||
|
msg['Date'] = formatdate(localtime=True) |
||||
|
msg['Subject'] = subject |
||||
|
|
||||
|
msg.attach( MIMEText(text) ) |
||||
|
|
||||
|
for file_name, file_content in files: |
||||
|
part = MIMEBase('application', "octet-stream") |
||||
|
part.set_payload( file_content ) |
||||
|
Encoders.encode_base64(part) |
||||
|
part.add_header('Content-Disposition', 'attachment; filename="%s"' |
||||
|
% file_name) |
||||
|
msg.attach(part) |
||||
|
|
||||
|
smtp = smtplib.SMTP(server, port=port) |
||||
|
smtp.sendmail(_from_, to_, msg.as_string() ) |
||||
|
smtp.close() |
||||
|
|
||||
|
class RPCProxy(object): |
||||
|
def __init__(self, uid, passwd, |
||||
|
host=config.OPENERP_HOSTNAME, |
||||
|
port=config.OPENERP_PORT, |
||||
|
path='object', |
||||
|
dbname=config.OPENERP_DEFAULT_DATABASE): |
||||
|
self.rpc = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (host, port, path), allow_none=True) |
||||
|
self.user_id = uid |
||||
|
self.passwd = passwd |
||||
|
self.dbname = dbname |
||||
|
|
||||
|
def __call__(self, *request, **kwargs): |
||||
|
return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request, **kwargs) |
||||
|
|
||||
|
class EmailParser(object): |
||||
|
def __init__(self, uid, password, dbname, host, port, model=False, email_default=False): |
||||
|
self.rpc = RPCProxy(uid, password, host=host, port=port, dbname=dbname) |
||||
|
if model: |
||||
|
try: |
||||
|
self.model_id = int(model) |
||||
|
self.model = str(model) |
||||
|
except: |
||||
|
self.model_id = self.rpc('ir.model', 'search', [('model', '=', model)])[0] |
||||
|
self.model = str(model) |
||||
|
self.email_default = email_default |
||||
|
|
||||
|
|
||||
|
def parse(self, message, custom_values=None, save_original=None): |
||||
|
# pass message as bytes because we don't know its encoding until we parse its headers |
||||
|
# and hence can't convert it to utf-8 for transport |
||||
|
return self.rpc('mail.thread', |
||||
|
'message_process', |
||||
|
self.model, |
||||
|
xmlrpclib.Binary(message), |
||||
|
custom_values or {}, |
||||
|
save_original or False) |
||||
|
|
||||
|
def configure_parser(): |
||||
|
parser = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.1') |
||||
|
group = optparse.OptionGroup(parser, "Note", |
||||
|
"This program parse a mail from standard input and communicate " |
||||
|
"with the Odoo server for case management in the CRM module.") |
||||
|
parser.add_option_group(group) |
||||
|
parser.add_option("-u", "--user", dest="userid", |
||||
|
help="Odoo user id to connect with", |
||||
|
default=config.OPENERP_DEFAULT_USER_ID, type='int') |
||||
|
parser.add_option("-p", "--password", dest="password", |
||||
|
help="Odoo user password", |
||||
|
default=config.OPENERP_DEFAULT_PASSWORD) |
||||
|
parser.add_option("-o", "--model", dest="model", |
||||
|
help="Name or ID of destination model", |
||||
|
default="crm.lead") |
||||
|
parser.add_option("-m", "--default", dest="default", |
||||
|
help="Admin email for error notifications.", |
||||
|
default=None) |
||||
|
parser.add_option("-d", "--dbname", dest="dbname", |
||||
|
help="Odoo database name (default: %default)", |
||||
|
default=config.OPENERP_DEFAULT_DATABASE) |
||||
|
parser.add_option("--host", dest="host", |
||||
|
help="Odoo Server hostname", |
||||
|
default=config.OPENERP_HOSTNAME) |
||||
|
parser.add_option("--port", dest="port", |
||||
|
help="Odoo Server XML-RPC port number", |
||||
|
default=config.OPENERP_PORT) |
||||
|
parser.add_option("--custom-values", dest="custom_values", |
||||
|
help="Dictionary of extra values to pass when creating records", |
||||
|
default=None) |
||||
|
parser.add_option("-s", dest="save_original", |
||||
|
action="store_true", |
||||
|
help="Keep a full copy of the email source attached to each message", |
||||
|
default=False) |
||||
|
|
||||
|
return parser |
||||
|
|
||||
|
def main(): |
||||
|
""" |
||||
|
Receive the email via the stdin and send it to the OpenERP Server |
||||
|
""" |
||||
|
|
||||
|
parser = configure_parser() |
||||
|
(options, args) = parser.parse_args() |
||||
|
email_parser = EmailParser(options.userid, |
||||
|
options.password, |
||||
|
options.dbname, |
||||
|
options.host, |
||||
|
options.port, |
||||
|
model=options.model, |
||||
|
email_default= options.default) |
||||
|
msg_txt = sys.stdin.read() |
||||
|
custom_values = {} |
||||
|
try: |
||||
|
custom_values = dict(eval(options.custom_values or "{}" )) |
||||
|
except: |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
|
||||
|
try: |
||||
|
email_parser.parse(msg_txt, custom_values, options.save_original or False) |
||||
|
except Exception: |
||||
|
msg = '\n'.join([ |
||||
|
'parameters', |
||||
|
'==========', |
||||
|
'%r' % (options,), |
||||
|
'traceback', |
||||
|
'=========', |
||||
|
'%s' % (cgitb.text(sys.exc_info())), |
||||
|
]) |
||||
|
|
||||
|
subject = '[Odoo]:ERROR: Mailgateway - %s' % time.strftime('%Y-%m-%d %H:%M:%S') |
||||
|
send_mail( |
||||
|
config.MAIL_ERROR, |
||||
|
config.MAIL_ADMINS, |
||||
|
subject, msg, files=[('message.txt', msg_txt)] |
||||
|
) |
||||
|
sys.stderr.write("Failed to deliver email to Odoo Server, sending error notification to %s\n" % config.MAIL_ADMINS) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
main() |
File diff suppressed because it is too large
@ -0,0 +1,666 @@ |
|||||
|
odoo.define('mail.chat_client_action', function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var chat_manager = require('mail.chat_manager'); |
||||
|
var composer = require('mail.composer'); |
||||
|
var ChatThread = require('mail.ChatThread'); |
||||
|
|
||||
|
var config = require('web.config'); |
||||
|
var ControlPanelMixin = require('web.ControlPanelMixin'); |
||||
|
var core = require('web.core'); |
||||
|
var data = require('web.data'); |
||||
|
var Dialog = require('web.Dialog'); |
||||
|
var framework = require('web.framework'); |
||||
|
var Model = require('web.Model'); |
||||
|
|
||||
|
var pyeval = require('web.pyeval'); |
||||
|
var SearchView = require('web.SearchView'); |
||||
|
var Widget = require('web.Widget'); |
||||
|
|
||||
|
var QWeb = core.qweb; |
||||
|
var _t = core._t; |
||||
|
|
||||
|
/** |
||||
|
* Widget : Invite People to Channel Dialog |
||||
|
* |
||||
|
* Popup containing a 'many2many_tags' custom input to select multiple partners. |
||||
|
* Search user according to the input, and trigger event when selection is validated. |
||||
|
**/ |
||||
|
var PartnerInviteDialog = Dialog.extend({ |
||||
|
dialog_title: _t('Invite people'), |
||||
|
template: "mail.PartnerInviteDialog", |
||||
|
init: function(parent, title, channel_id){ |
||||
|
this.channel_id = channel_id; |
||||
|
|
||||
|
this._super(parent, { |
||||
|
title: title, |
||||
|
size: "medium", |
||||
|
buttons: [{ |
||||
|
text: _t("Invite"), |
||||
|
close: true, |
||||
|
classes: "btn-primary", |
||||
|
click: _.bind(this.on_click_add, this), |
||||
|
}], |
||||
|
}); |
||||
|
this.PartnersModel = new Model('res.partner'); |
||||
|
}, |
||||
|
start: function(){ |
||||
|
var self = this; |
||||
|
this.$input = this.$('.o_mail_chat_partner_invite_input'); |
||||
|
this.$input.select2({ |
||||
|
width: '100%', |
||||
|
allowClear: true, |
||||
|
multiple: true, |
||||
|
formatResult: function(item) { |
||||
|
var status = QWeb.render('mail.chat.UserStatus', {status: item.im_status}); |
||||
|
return $('<span>').text(item.text).prepend(status); |
||||
|
}, |
||||
|
query: function (query) { |
||||
|
self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){ |
||||
|
var data = []; |
||||
|
_.each(result, function(partner){ |
||||
|
partner.text = partner.name; |
||||
|
data.push(partner); |
||||
|
}); |
||||
|
query.callback({results: data}); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
return this._super.apply(this, arguments); |
||||
|
}, |
||||
|
on_click_add: function(){ |
||||
|
var self = this; |
||||
|
var data = this.$input.select2('data'); |
||||
|
if(data.length >= 1){ |
||||
|
var ChannelModel = new Model('mail.channel'); |
||||
|
return ChannelModel.call('channel_invite', [], {ids : [this.channel_id], partner_ids: _.pluck(data, 'id')}) |
||||
|
.then(function(){ |
||||
|
var names = _.escape(_.pluck(data, 'text').join(', ')); |
||||
|
var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names); |
||||
|
self.do_notify(_t('New people'), notification); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
var ChatAction = Widget.extend(ControlPanelMixin, { |
||||
|
template: 'mail.client_action', |
||||
|
|
||||
|
events: { |
||||
|
"click .o_mail_chat_channel_item": function (event) { |
||||
|
event.preventDefault(); |
||||
|
var channel_id = this.$(event.currentTarget).data('channel-id'); |
||||
|
this.set_channel(chat_manager.get_channel(channel_id)); |
||||
|
}, |
||||
|
"click .o_mail_sidebar_title .o_add": function (event) { |
||||
|
event.preventDefault(); |
||||
|
var type = $(event.target).data("type"); |
||||
|
this.$('.o_mail_add_channel[data-type=' + type + ']') |
||||
|
.show() |
||||
|
.find("input").focus(); |
||||
|
}, |
||||
|
"blur .o_mail_add_channel input": function () { |
||||
|
this.$('.o_mail_add_channel') |
||||
|
.hide(); |
||||
|
}, |
||||
|
"click .o_mail_partner_unpin": function (event) { |
||||
|
event.stopPropagation(); |
||||
|
var channel_id = $(event.target).data("channel-id"); |
||||
|
this.unsubscribe_from_channel(chat_manager.get_channel(channel_id)); |
||||
|
}, |
||||
|
"click .o_snackbar_undo": function (event) { |
||||
|
event.preventDefault(); |
||||
|
var channel = this.channel; |
||||
|
this.$snackbar.remove(); |
||||
|
this.clear_needactions_def.then(function (msgs_ids) { |
||||
|
chat_manager.undo_mark_as_read(msgs_ids, channel); |
||||
|
}); |
||||
|
}, |
||||
|
"click .o_mail_annoying_notification_bar .fa-close": function () { |
||||
|
this.$(".o_mail_annoying_notification_bar").slideUp(); |
||||
|
}, |
||||
|
"click .o_mail_request_permission": function (event) { |
||||
|
event.preventDefault(); |
||||
|
this.$(".o_mail_annoying_notification_bar").slideUp(); |
||||
|
var def = window.Notification.requestPermission(); |
||||
|
if (def) { |
||||
|
def.then(function () { |
||||
|
chat_manager.send_native_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.'); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
"keydown": function (event) { |
||||
|
if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) { |
||||
|
this.unselect_message(); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
on_attach_callback: function () { |
||||
|
chat_manager.bus.trigger('client_action_open', true); |
||||
|
if (this.channel) { |
||||
|
this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); |
||||
|
} |
||||
|
}, |
||||
|
on_detach_callback: function () { |
||||
|
chat_manager.bus.trigger('client_action_open', false); |
||||
|
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); |
||||
|
}, |
||||
|
|
||||
|
init: function(parent, action, options) { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.action_manager = parent; |
||||
|
this.domain = []; |
||||
|
this.action = action; |
||||
|
this.options = options || {}; |
||||
|
this.channels_scrolltop = {}; |
||||
|
this.throttled_render_sidebar = _.throttle(this.render_sidebar.bind(this), 100, { leading: false }); |
||||
|
this.notification_bar = (window.Notification && window.Notification.permission === "default"); |
||||
|
this.selected_message = null; |
||||
|
}, |
||||
|
|
||||
|
willStart: function () { |
||||
|
return chat_manager.is_ready; |
||||
|
}, |
||||
|
|
||||
|
start: function() { |
||||
|
var self = this; |
||||
|
|
||||
|
// create searchview
|
||||
|
var options = { |
||||
|
$buttons: $("<div>"), |
||||
|
action: this.action, |
||||
|
disable_groupby: true, |
||||
|
}; |
||||
|
var dataset = new data.DataSetSearch(this, 'mail.message'); |
||||
|
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; |
||||
|
var default_channel_id = this.options.active_id || |
||||
|
this.action.context.active_id || |
||||
|
this.action.params.default_active_id || |
||||
|
'channel_inbox'; |
||||
|
var default_channel = chat_manager.get_channel(default_channel_id) || |
||||
|
chat_manager.get_channel('channel_inbox'); |
||||
|
|
||||
|
this.searchview = new SearchView(this, dataset, view_id, {}, options); |
||||
|
this.searchview.on('search_data', this, this.on_search); |
||||
|
|
||||
|
this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true}); |
||||
|
this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true}); |
||||
|
this.thread = new ChatThread(this, { |
||||
|
display_help: true, |
||||
|
shorten_messages: false, |
||||
|
}); |
||||
|
|
||||
|
this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); |
||||
|
this.$buttons.find('button').css({display:"inline-block"}); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_unsubscribe', this.on_click_button_unsubscribe); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_settings', this.on_click_button_settings); |
||||
|
this.$buttons.on('click', '.o_mail_toggle_channels', function () { |
||||
|
self.$('.o_mail_chat_sidebar').slideToggle(200); |
||||
|
}); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () { |
||||
|
chat_manager.mark_all_as_read(self.channel, self.domain); |
||||
|
}); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message); |
||||
|
this.$buttons.on('click', '.o_mail_chat_button_new_message_sent', this.on_click_new_message); |
||||
|
|
||||
|
this.thread.on('redirect', this, function (res_model, res_id) { |
||||
|
chat_manager.redirect(res_model, res_id, this.set_channel.bind(this)); |
||||
|
}); |
||||
|
this.thread.on('redirect_to_channel', this, function (channel_id) { |
||||
|
chat_manager.join_channel(channel_id).then(this.set_channel.bind(this)); |
||||
|
}); |
||||
|
this.thread.on('load_more_messages', this, this.load_more_messages); |
||||
|
this.thread.on('mark_as_read', this, function (message_id) { |
||||
|
chat_manager.mark_as_read([message_id]); |
||||
|
}); |
||||
|
this.thread.on('toggle_star_status', this, function (message_id) { |
||||
|
chat_manager.toggle_star_status(message_id); |
||||
|
}); |
||||
|
this.thread.on('select_message', this, this.select_message); |
||||
|
this.thread.on('unselect_message', this, this.unselect_message); |
||||
|
|
||||
|
this.basic_composer.on('post_message', this, this.on_post_message); |
||||
|
this.basic_composer.on('input_focused', this, this.on_composer_input_focused); |
||||
|
this.extended_composer.on('post_message', this, this.on_post_message); |
||||
|
this.extended_composer.on('input_focused', this, this.on_composer_input_focused); |
||||
|
|
||||
|
var def1 = this.thread.appendTo(this.$('.o_mail_chat_content')); |
||||
|
var def2 = this.basic_composer.appendTo(this.$('.o_mail_chat_content')); |
||||
|
var def3 = this.extended_composer.appendTo(this.$('.o_mail_chat_content')); |
||||
|
var def4 = this.searchview.appendTo($("<div>")).then(function () { |
||||
|
self.$searchview_buttons = self.searchview.$buttons.contents(); |
||||
|
}); |
||||
|
|
||||
|
this.render_sidebar(); |
||||
|
|
||||
|
return $.when(def1, def2, def3, def4) |
||||
|
.then(this.set_channel.bind(this, default_channel)) |
||||
|
.then(function () { |
||||
|
chat_manager.bus.on('open_channel', self, self.set_channel); |
||||
|
chat_manager.bus.on('new_message', self, self.on_new_message); |
||||
|
chat_manager.bus.on('update_message', self, self.on_update_message); |
||||
|
chat_manager.bus.on('new_channel', self, self.on_new_channel); |
||||
|
chat_manager.bus.on('anyone_listening', self, function (channel, query) { |
||||
|
query.is_displayed = query.is_displayed || (channel.id === self.channel.id && self.thread.is_at_bottom()); |
||||
|
}); |
||||
|
chat_manager.bus.on('unsubscribe_from_channel', self, self.render_sidebar); |
||||
|
chat_manager.bus.on('update_needaction', self, self.throttled_render_sidebar); |
||||
|
chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_render_sidebar); |
||||
|
chat_manager.bus.on('update_dm_presence', self, self.throttled_render_sidebar); |
||||
|
self.thread.$el.on("scroll", null, _.debounce(function () { |
||||
|
if (self.thread.is_at_bottom()) { |
||||
|
chat_manager.mark_channel_as_seen(self.channel); |
||||
|
} |
||||
|
}, 100)); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
select_message: function(message_id) { |
||||
|
this.$el.addClass('o_mail_selection_mode'); |
||||
|
var message = chat_manager.get_message(message_id); |
||||
|
this.selected_message = message; |
||||
|
var subject = "Re: " + message.record_name; |
||||
|
this.extended_composer.set_subject(subject); |
||||
|
if (this.channel.type !== 'static') { |
||||
|
this.basic_composer.toggle(false); |
||||
|
} |
||||
|
this.extended_composer.toggle(true); |
||||
|
this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true}); |
||||
|
this.extended_composer.focus('body'); |
||||
|
}, |
||||
|
|
||||
|
unselect_message: function() { |
||||
|
this.basic_composer.toggle(this.channel.type !== 'static' && !this.channel.mass_mailing); |
||||
|
this.extended_composer.toggle(this.channel.type !== 'static' && this.channel.mass_mailing); |
||||
|
if (!config.device.touch) { |
||||
|
var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; |
||||
|
composer.focus(); |
||||
|
} |
||||
|
this.$el.removeClass('o_mail_selection_mode'); |
||||
|
this.thread.unselect(); |
||||
|
this.selected_message = null; |
||||
|
}, |
||||
|
|
||||
|
render_sidebar: function () { |
||||
|
var self = this; |
||||
|
var $sidebar = $(QWeb.render("mail.chat.Sidebar", { |
||||
|
active_channel_id: this.channel ? this.channel.id: undefined, |
||||
|
channels: chat_manager.get_channels(), |
||||
|
needaction_counter: chat_manager.get_needaction_counter(), |
||||
|
})); |
||||
|
this.$(".o_mail_chat_sidebar").html($sidebar.contents()); |
||||
|
|
||||
|
this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({ |
||||
|
source: function(request, response) { |
||||
|
self.last_search_val = _.escape(request.term); |
||||
|
self.do_search_channel(self.last_search_val).done(function(result){ |
||||
|
result.push({ |
||||
|
'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"#'+self.last_search_val+'"</em>'), |
||||
|
'value': '_create', |
||||
|
}); |
||||
|
response(result); |
||||
|
}); |
||||
|
}, |
||||
|
select: function(event, ui) { |
||||
|
if (self.last_search_val) { |
||||
|
if (ui.item.value === '_create') { |
||||
|
chat_manager.create_channel(self.last_search_val, "public"); |
||||
|
} else { |
||||
|
chat_manager.join_channel(ui.item.id); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
focus: function(event) { |
||||
|
event.preventDefault(); |
||||
|
}, |
||||
|
html: true, |
||||
|
}); |
||||
|
|
||||
|
this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({ |
||||
|
source: function(request, response) { |
||||
|
self.last_search_val = _.escape(request.term); |
||||
|
chat_manager.search_partner(self.last_search_val).done(response); |
||||
|
}, |
||||
|
select: function(event, ui) { |
||||
|
var partner_id = ui.item.id; |
||||
|
chat_manager.create_channel(partner_id, "dm"); |
||||
|
}, |
||||
|
focus: function(event) { |
||||
|
event.preventDefault(); |
||||
|
}, |
||||
|
html: true, |
||||
|
}); |
||||
|
|
||||
|
this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) { |
||||
|
var name = _.escape($(event.target).val()); |
||||
|
if(event.which === $.ui.keyCode.ENTER && name) { |
||||
|
chat_manager.create_channel(name, "private"); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
render_snackbar: function (template, context, timeout) { |
||||
|
if (this.$snackbar) { |
||||
|
this.$snackbar.remove(); |
||||
|
} |
||||
|
timeout = timeout || 20000; |
||||
|
this.$snackbar = $(QWeb.render(template, context)); |
||||
|
this.$('.o_mail_chat_content').append(this.$snackbar); |
||||
|
// Hide snackbar after [timeout] milliseconds (by default, 20s)
|
||||
|
var $snackbar = this.$snackbar; |
||||
|
setTimeout(function() { $snackbar.fadeOut(); }, timeout); |
||||
|
}, |
||||
|
|
||||
|
do_search_channel: function(search_val){ |
||||
|
var Channel = new Model("mail.channel"); |
||||
|
return Channel.call('channel_search_to_join', [search_val]).then(function(result){ |
||||
|
var values = []; |
||||
|
_.each(result, function(channel){ |
||||
|
var escaped_name = _.escape(channel.name); |
||||
|
values.push(_.extend(channel, { |
||||
|
'value': escaped_name, |
||||
|
'label': escaped_name, |
||||
|
})); |
||||
|
}); |
||||
|
return values; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
set_channel: function (channel) { |
||||
|
var self = this; |
||||
|
// Store scroll position of previous channel
|
||||
|
if (this.channel) { |
||||
|
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); |
||||
|
} |
||||
|
var new_channel_scrolltop = this.channels_scrolltop[channel.id]; |
||||
|
|
||||
|
this.channel = channel; |
||||
|
this.messages_separator_position = undefined; // reset value on channel change
|
||||
|
this.unread_counter = this.channel.unread_counter; |
||||
|
this.last_seen_message_id = this.channel.last_seen_message_id; |
||||
|
this.clear_needactions_def = $.Deferred(); |
||||
|
if (this.$snackbar) { |
||||
|
this.$snackbar.remove(); |
||||
|
} |
||||
|
|
||||
|
this.action.context.active_id = channel.id; |
||||
|
this.action.context.active_ids = [channel.id]; |
||||
|
|
||||
|
return this.fetch_and_render_thread().then(function () { |
||||
|
// Mark channel's messages as read and clear needactions
|
||||
|
if (channel.type !== 'static') { |
||||
|
// Display snackbar if needactions have been cleared
|
||||
|
if (channel.needaction_counter > 0) { |
||||
|
self.render_snackbar('mail.chat.UndoSnackbar', { |
||||
|
nb_needactions: channel.needaction_counter, |
||||
|
}); |
||||
|
} |
||||
|
chat_manager.mark_channel_as_seen(channel); |
||||
|
self.clear_needactions_def = chat_manager.mark_all_as_read(channel); |
||||
|
} |
||||
|
|
||||
|
// Update control panel
|
||||
|
self.set("title", '#' + channel.name); |
||||
|
// Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM
|
||||
|
self.$buttons |
||||
|
.find('.o_mail_chat_button_invite, .o_mail_chat_button_unsubscribe, .o_mail_chat_button_settings') |
||||
|
.toggle(channel.type !== "dm" && channel.type !== 'static'); |
||||
|
self.$buttons |
||||
|
.find('.o_mail_chat_button_mark_read, .o_mail_chat_button_new_message') |
||||
|
.toggle(channel.id === "channel_inbox"); |
||||
|
self.$buttons |
||||
|
.find('.o_mail_chat_button_new_message_sent') |
||||
|
.toggle(channel.id === "channel_sent"); |
||||
|
self.$buttons |
||||
|
.find('.o_mail_chat_button_unstar_all') |
||||
|
.toggle(channel.id === "channel_starred"); |
||||
|
|
||||
|
self.$('.o_mail_chat_channel_item') |
||||
|
.removeClass('o_active') |
||||
|
.filter('[data-channel-id=' + channel.id + ']') |
||||
|
.removeClass('o_unread_message') |
||||
|
.addClass('o_active'); |
||||
|
|
||||
|
var $new_messages_separator = self.$('.o_thread_new_messages_separator'); |
||||
|
if ($new_messages_separator.length) { |
||||
|
self.thread.$el.scrollTo($new_messages_separator); |
||||
|
} else { |
||||
|
self.thread.scroll_to({offset: new_channel_scrolltop}); |
||||
|
} |
||||
|
|
||||
|
// Update control panel before focusing the composer, otherwise focus is on the searchview
|
||||
|
self.update_cp(); |
||||
|
if (config.device.size_class === config.device.SIZES.XS) { |
||||
|
self.$('.o_mail_chat_sidebar').hide(); |
||||
|
} |
||||
|
|
||||
|
// Display and focus the adequate composer, and unselect possibly selected message
|
||||
|
// to prevent sending messages as reply to that message
|
||||
|
self.unselect_message(); |
||||
|
|
||||
|
self.action_manager.do_push_state({ |
||||
|
action: self.action.id, |
||||
|
active_id: self.channel.id, |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
unsubscribe_from_channel: function (channel) { |
||||
|
var self = this; |
||||
|
chat_manager |
||||
|
.unsubscribe(channel) |
||||
|
.then(this.render_sidebar.bind(this)) |
||||
|
.then(this.set_channel.bind(this, chat_manager.get_channel("channel_inbox"))) |
||||
|
.then(function () { |
||||
|
if (_.contains(['public', 'private'], channel.type)) { |
||||
|
var msg = _.str.sprintf(_t('You unsubscribed from <b>%s</b>.'), channel.name); |
||||
|
self.do_notify(_t("Unsubscribed"), msg); |
||||
|
} |
||||
|
delete self.channels_scrolltop[channel.id]; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
get_thread_rendering_options: function (messages) { |
||||
|
// Compute position of the 'New messages' separator, only once when joining
|
||||
|
// a channel to keep it in the thread when new messages arrive
|
||||
|
if (_.isUndefined(this.messages_separator_position)) { |
||||
|
if (!this.unread_counter) { |
||||
|
this.messages_separator_position = false; // no unread message -> don't display separator
|
||||
|
} else { |
||||
|
var msg = chat_manager.get_last_seen_message(this.channel); |
||||
|
this.messages_separator_position = msg ? msg.id : 'top'; |
||||
|
} |
||||
|
} |
||||
|
return { |
||||
|
channel_id: this.channel.id, |
||||
|
display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain), |
||||
|
display_needactions: this.channel.display_needactions, |
||||
|
messages_separator_position: this.messages_separator_position, |
||||
|
squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing, |
||||
|
display_empty_channel: !messages.length && !this.domain.length, |
||||
|
display_no_match: !messages.length && this.domain.length, |
||||
|
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", |
||||
|
display_reply_icon: true, |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
fetch_and_render_thread: function () { |
||||
|
var self = this; |
||||
|
return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function(result) { |
||||
|
self.thread.render(result, self.get_thread_rendering_options(result)); |
||||
|
self.update_button_status(result.length === 0); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
update_button_status: function (disabled) { |
||||
|
if (this.channel.id === "channel_inbox") { |
||||
|
this.$buttons |
||||
|
.find('.o_mail_chat_button_mark_read') |
||||
|
.toggleClass('disabled', disabled); |
||||
|
} |
||||
|
if (this.channel.id === "channel_starred") { |
||||
|
this.$buttons |
||||
|
.find('.o_mail_chat_button_unstar_all') |
||||
|
.toggleClass('disabled', disabled); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
load_more_messages: function () { |
||||
|
var self = this; |
||||
|
var oldest_msg_id = this.$('.o_thread_message').first().data('messageId'); |
||||
|
var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]'; |
||||
|
var offset = -framework.getPosition(document.querySelector(oldest_msg_selector)).top; |
||||
|
return chat_manager |
||||
|
.get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true}) |
||||
|
.then(function(result) { |
||||
|
if (self.messages_separator_position === 'top') { |
||||
|
self.messages_separator_position = undefined; // reset value to re-compute separator position
|
||||
|
} |
||||
|
self.thread.render(result, self.get_thread_rendering_options(result)); |
||||
|
offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top; |
||||
|
self.thread.scroll_to({offset: offset}); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
update_cp: function () { |
||||
|
this.update_control_panel({ |
||||
|
breadcrumbs: this.action_manager.get_breadcrumbs(), |
||||
|
cp_content: { |
||||
|
$buttons: this.$buttons, |
||||
|
$searchview: this.searchview.$el, |
||||
|
$searchview_buttons: this.$searchview_buttons, |
||||
|
}, |
||||
|
searchview: this.searchview, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
do_show: function () { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.update_cp(); |
||||
|
this.action_manager.do_push_state({ |
||||
|
action: this.action.id, |
||||
|
active_id: this.channel.id, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
on_search: function (domains) { |
||||
|
var result = pyeval.sync_eval_domains_and_contexts({ |
||||
|
domains: domains |
||||
|
}); |
||||
|
|
||||
|
this.domain = result.domain; |
||||
|
this.fetch_and_render_thread(); |
||||
|
}, |
||||
|
|
||||
|
on_post_message: function (message) { |
||||
|
var self = this; |
||||
|
var options = this.selected_message ? {} : {channel_id: this.channel.id}; |
||||
|
if (this.selected_message) { |
||||
|
message.subtype = 'mail.mt_comment'; |
||||
|
message.subtype_id = false; |
||||
|
message.message_type = 'comment'; |
||||
|
message.content_subtype = 'html'; |
||||
|
|
||||
|
options.model = this.selected_message.model; |
||||
|
options.res_id = this.selected_message.res_id; |
||||
|
} |
||||
|
chat_manager |
||||
|
.post_message(message, options) |
||||
|
.then(function() { |
||||
|
if (self.selected_message) { |
||||
|
self.render_snackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000); |
||||
|
self.unselect_message(); |
||||
|
} else { |
||||
|
self.thread.scroll_to(); |
||||
|
} |
||||
|
}) |
||||
|
.fail(function () { |
||||
|
// todo: display notification
|
||||
|
}); |
||||
|
}, |
||||
|
on_new_message: function (message) { |
||||
|
var self = this; |
||||
|
if (_.contains(message.channel_ids, this.channel.id)) { |
||||
|
if (this.channel.type !== 'static' && this.thread.is_at_bottom()) { |
||||
|
chat_manager.mark_channel_as_seen(this.channel); |
||||
|
} |
||||
|
|
||||
|
var should_scroll = this.thread.is_at_bottom(); |
||||
|
this.fetch_and_render_thread().then(function () { |
||||
|
if (should_scroll) { |
||||
|
self.thread.scroll_to({id: message.id}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
// Re-render sidebar to indicate that there is a new message in the corresponding channels
|
||||
|
this.render_sidebar(); |
||||
|
// Dump scroll position of channels in which the new message arrived
|
||||
|
this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids); |
||||
|
}, |
||||
|
on_update_message: function (message) { |
||||
|
var self = this; |
||||
|
var current_channel_id = this.channel.id; |
||||
|
if ((current_channel_id === "channel_starred" && !message.is_starred) || |
||||
|
(current_channel_id === "channel_inbox" && !message.is_needaction)) { |
||||
|
chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) { |
||||
|
var options = self.get_thread_rendering_options(messages); |
||||
|
self.thread.remove_message_and_render(message.id, messages, options).then(function () { |
||||
|
self.update_button_status(messages.length === 0); |
||||
|
}); |
||||
|
}); |
||||
|
} else if (_.contains(message.channel_ids, current_channel_id)) { |
||||
|
this.fetch_and_render_thread(); |
||||
|
} |
||||
|
}, |
||||
|
on_new_channel: function (channel) { |
||||
|
this.render_sidebar(); |
||||
|
if (channel.autoswitch) { |
||||
|
this.set_channel(channel); |
||||
|
} |
||||
|
}, |
||||
|
on_composer_input_focused: function () { |
||||
|
var suggestions = chat_manager.get_mention_partner_suggestions(this.channel); |
||||
|
var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; |
||||
|
composer.mention_set_prefetched_partners(suggestions); |
||||
|
}, |
||||
|
|
||||
|
on_click_button_invite: function () { |
||||
|
var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); |
||||
|
new PartnerInviteDialog(this, title, this.channel.id).open(); |
||||
|
}, |
||||
|
|
||||
|
on_click_button_unsubscribe: function () { |
||||
|
this.unsubscribe_from_channel(this.channel); |
||||
|
}, |
||||
|
on_click_button_settings: function() { |
||||
|
this.do_action({ |
||||
|
type: 'ir.actions.act_window', |
||||
|
res_model: "mail.channel", |
||||
|
res_id: this.channel.id, |
||||
|
views: [[false, 'form']], |
||||
|
target: 'current' |
||||
|
}); |
||||
|
}, |
||||
|
on_click_new_message: function () { |
||||
|
this.do_action({ |
||||
|
type: 'ir.actions.act_window', |
||||
|
res_model: 'mail.compose.message', |
||||
|
view_mode: 'form', |
||||
|
view_type: 'form', |
||||
|
views: [[false, 'form']], |
||||
|
target: 'new', |
||||
|
context: "{'default_no_auto_thread': False, 'active_model': 'mail.message'}", |
||||
|
}); |
||||
|
}, |
||||
|
destroy: function() { |
||||
|
this.$buttons.off().destroy(); |
||||
|
this._super.apply(this, arguments); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
core.action_registry.add('mail.chat.instant_messaging', ChatAction); |
||||
|
|
||||
|
}); |
@ -0,0 +1,19 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<templates id="template" xml:space="preserve"> |
||||
|
|
||||
|
<t t-extend="mail.chat.Sidebar"> |
||||
|
<t t-jquery="div.o_mail_chat_sidebar" t-operation="prepend"> |
||||
|
<div t-attf-class="o_mail_chat_channel_item #{(active_channel_id == 'channel_sent') ? 'o_active': ''}" |
||||
|
data-channel-id="channel_sent"> |
||||
|
<span class="o_channel_name"> <i class="fa fa-envelope-o"/> Sent </span> |
||||
|
</div> |
||||
|
</t> |
||||
|
</t> |
||||
|
|
||||
|
<t t-extend="mail.chat.ControlButtons"> |
||||
|
<t t-jquery="button.o_mail_chat_button_new_message" t-operation="before"> |
||||
|
<button type="button" class="btn btn-primary btn-sm o_mail_chat_button_new_message_sent" title="Compose new messages">Send mails</button> |
||||
|
</t> |
||||
|
</t> |
||||
|
|
||||
|
</templates> |
@ -0,0 +1,27 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<template id="mail.assets_backend" name="mail assets" inherit_id="web.assets_backend"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/many2many_tags_email.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/announcement.js"></script> |
||||
|
|
||||
|
<script type="text/javascript" src="/sent_mails/static/src/js/client_action_sent_mail.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/composer.js"></script> |
||||
|
<script type="text/javascript" src="/sent_mails/static/src/js/chat_manager_sent_mail.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/chatter.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/systray.js"></script> |
||||
|
<script type="text/javascript" src="/mail/static/src/js/window_manager.js"></script> |
||||
|
|
||||
|
<link rel="stylesheet" href="/mail/static/src/less/announcement.less"/> |
||||
|
<link rel="stylesheet" href="/mail/static/src/less/client_action.less" type="text/less"/> |
||||
|
<link rel="stylesheet" href="/mail/static/src/less/chat_window.less" type="text/less"/> |
||||
|
<link rel="stylesheet" href="/mail/static/src/less/composer.less" type="text/less"/> |
||||
|
<link rel="stylesheet" href="/mail/static/src/less/chatter.less" type="text/less"/> |
||||
|
<link rel="stylesheet" href="/mail/static/src/less/thread.less" type="text/less"/> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
</data> |
||||
|
</openerp> |
Loading…
Reference in new issue