diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index d730d039050..a2c7c543f54 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -110,6 +110,7 @@ class MailThread(models.AbstractModel):
message_has_error_counter = fields.Integer(
'Number of error', compute='_compute_message_has_error',
help="Number of messages with delivery error")
+ related_attachment_count = fields.Integer('Attachment Count', compute='_compute_related_attachment_count')
@api.one
@api.depends('message_follower_ids')
@@ -145,6 +146,12 @@ class MailThread(models.AbstractModel):
# using read() below is much faster than followers.mapped('res_id')
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
+ @api.multi
+ def _compute_related_attachment_count(self):
+ for record in self:
+ domain = [('res_id', '=', record.id), ('res_model', '=', self._name)]
+ record.related_attachment_count = self.env['ir.attachment'].search_count(domain)
+
@api.multi
@api.depends('message_follower_ids')
def _compute_is_follower(self):
diff --git a/addons/mail/static/src/js/attachment_box.js b/addons/mail/static/src/js/attachment_box.js
new file mode 100644
index 00000000000..b42e8aeca95
--- /dev/null
+++ b/addons/mail/static/src/js/attachment_box.js
@@ -0,0 +1,99 @@
+odoo.define('mail.AttachmentBox', function (require) {
+"use strict";
+
+var core = require('web.core');
+var Widget = require('web.Widget');
+
+var DocumentViewer = require('mail.DocumentViewer');
+
+var QWeb = core.qweb;
+
+var AttachmentBox = Widget.extend({
+ template: 'mail.chatter.AttachmentBox',
+ events: {
+ "click .o_attachment_download": "_onAttachmentDownload",
+ "click .o_attachment_view": "_onAttachmentView",
+ },
+ /**
+ * @override
+ * @param {string} record.model
+ * @param {Number} record.res_id
+ */
+ init: function (parent, record) {
+ this._super.apply(this, arguments);
+ this.currentResID = record.res_id;
+ this.currentResModel = record.model;
+ this.attachmentIDs = {};
+ this.imageList = {};
+ this.otherList = {};
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ var self = this;
+ var domain = [
+ ['res_id', '=', this.currentResID],
+ ['res_model', '=', this.currentResModel],
+ ];
+ return $.when(this._super.apply(this, arguments), this._rpc({
+ model: 'ir.attachment',
+ method: 'search_read',
+ domain: domain,
+ }).then(function (result) {
+ self.attachmentIDs = result;
+ _.each(result, function (attachment) {
+ attachment.url = '/web/content/' + attachment.id + '?download=true';
+ // required for compatibility with the chatter templates.
+ attachment.filename = attachment.datas_fname || 'unnamed';
+ });
+ var sortedAttachments = _.partition(result, function (att) {
+ return att.mimetype && att.mimetype.split('/')[0] === 'image';
+ });
+ self.imageList = sortedAttachments[0];
+ self.otherList = sortedAttachments[1];
+ }));
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @param {Object} record
+ */
+ update: function (record) {
+ this.currentResID = record.res_id;
+ this.currentResModel = record.model;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ * used to prevent the click from opening the document viewer.
+ */
+ _onAttachmentDownload: function (ev) {
+ ev.stopPropagation();
+ },
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAttachmentView: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var activeAttachmentID = $(ev.currentTarget).data('id');
+ if (activeAttachmentID) {
+ var attachmentViewer = new DocumentViewer(this, this.attachmentIDs, activeAttachmentID);
+ attachmentViewer.appendTo($('body'));
+ }
+ },
+});
+
+return AttachmentBox;
+
+});
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index ea0f3dd8e4b..0814723caf0 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -2,6 +2,7 @@ odoo.define('mail.Chatter', function (require) {
"use strict";
var Activity = require('mail.Activity');
+var AttachmentBox = require('mail.AttachmentBox');
var ChatterComposer = require('mail.composer.Chatter');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
@@ -30,6 +31,7 @@ var Chatter = Widget.extend({
events: {
'click .o_chatter_button_new_message': '_onOpenComposerMessage',
'click .o_chatter_button_log_note': '_onOpenComposerNote',
+ 'click .o_chatter_button_attachment': '_onOpenAttachments',
'click .o_chatter_button_schedule_activity': '_onScheduleActivity',
},
supportedFieldTypes: ['one2many'],
@@ -69,21 +71,23 @@ var Chatter = Widget.extend({
this.hasLogButton = options.display_log_button || nodeOptions.display_log_button;
this.postRefresh = nodeOptions.post_refresh || 'never';
}
+ this.attachmentBoxOpened = false;
},
/**
* @override
*/
start: function () {
this._$topbar = this.$('.o_chatter_topbar');
-
+ this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', {
+ count: this.record.data.related_attachment_count || 0,
+ }));
// render and append the buttons
- this._$topbar.append(QWeb.render('mail.chatter.Buttons', {
+ this._$topbar.prepend(QWeb.render('mail.chatter.Buttons', {
newMessageButton: !!this.fields.thread,
logNoteButton: this.hasLogButton,
scheduleActivityButton: !!this.fields.activity,
isMobile: config.device.isMobile,
}));
-
// start and append the widgets
var fieldDefs = _.invoke(this.fields, 'appendTo', $('
'));
var def = this._dp.add($.when.apply($, fieldDefs));
@@ -107,6 +111,7 @@ var Chatter = Widget.extend({
// close the composer if we switch to another record as it is record dependent
if (this.record.res_id !== record.res_id) {
this._closeComposer(true);
+ this._closeAttachments();
}
// update the state
@@ -138,12 +143,24 @@ var Chatter = Widget.extend({
self.$el.height('auto');
self._updateMentionSuggestions();
});
+ this._updateAttachmentCounter();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ /**
+ * @private
+ * @param {boolean} force
+ */
+ _closeAttachments: function () {
+ if (this.fields.attachments) {
+ this.$('.o_chatter_button_attachment').removeClass('o_active_attach');
+ this.fields.attachments.destroy();
+ this.attachmentBoxOpened = false;
+ }
+ },
/**
* @private
* @param {boolean} force
@@ -199,6 +216,23 @@ var Chatter = Widget.extend({
_enableChatter: function () {
this.$('.btn').prop('disabled', false); // enable buttons
},
+ /**
+ * @private
+ */
+ _openAttachments: function () {
+ var self = this;
+ this.fields.attachments = new AttachmentBox(this, this.record);
+
+ var $anchor = this.$('.o_chatter_topbar');
+ if (this._composer) {
+ $anchor = this.$('.o_thread_composer');
+ }
+ this.fields.attachments.insertAfter($anchor).then(function () {
+ self.$el.addClass('o_chatter_composer_active');
+ self.$('.o_chatter_button_attachment').addClass('o_active_attach');
+ });
+ this.attachmentBoxOpened = true;
+ },
/**
* @private
* @param {Object} options
@@ -235,6 +269,8 @@ var Chatter = Widget.extend({
self._closeComposer(true);
if (self._reloadAfterPost(messageData)) {
self.trigger_up('reload');
+ } else if (messageData.attachment_ids.length) {
+ self.trigger_up('reload', {fieldNames: ['related_attachment_count']});
}
});
});
@@ -290,7 +326,7 @@ var Chatter = Widget.extend({
self.fields.activity.$el.appendTo(self.$el);
}
if (self.fields.followers) {
- self.fields.followers.$el.appendTo(self._$topbar);
+ self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
}
if (self.fields.thread) {
self.fields.thread.$el.appendTo(self.$el);
@@ -324,6 +360,14 @@ var Chatter = Widget.extend({
this.record = record;
this.recordName = record.data.display_name;
},
+ /**
+ * @private
+ */
+ _updateAttachmentCounter: function () {
+ var count = this.record.data.related_attachment_count || 0;
+ this.$('.o_chatter_attachment_button_count').html(' ('+ count +')');
+ this.$('.o_chatter_button_attachment').toggleClass('o_hidden', !count);
+ },
/**
* @private
*/
@@ -363,6 +407,16 @@ var Chatter = Widget.extend({
// Handlers
//--------------------------------------------------------------------------
+ /**
+ * @private
+ */
+ _onOpenAttachments: function () {
+ if (this.attachmentBoxOpened) {
+ this._closeAttachments();
+ } else {
+ this._openAttachments();
+ }
+ },
/**
* Discard changes on the record.
* This is notified by the composer, when opening the full-composer.
diff --git a/addons/mail/static/src/js/form_renderer.js b/addons/mail/static/src/js/form_renderer.js
index 47a2b6b795e..e01bdf18633 100644
--- a/addons/mail/static/src/js/form_renderer.js
+++ b/addons/mail/static/src/js/form_renderer.js
@@ -29,7 +29,8 @@ FormRenderer.include({
*/
confirmChange: function (state, id, fields) {
if (this.chatter) {
- var updatedMailFields = _.intersection(fields, _.values(this.mailFields));
+ var chatterFields = ['related_attachment_count'].concat(_.values(this.mailFields));
+ var updatedMailFields = _.intersection(fields, chatterFields);
if (updatedMailFields.length) {
this.chatter.update(state, updatedMailFields);
}
diff --git a/addons/mail/static/src/js/form_view.js b/addons/mail/static/src/js/form_view.js
new file mode 100644
index 00000000000..2eaed79e8c0
--- /dev/null
+++ b/addons/mail/static/src/js/form_view.js
@@ -0,0 +1,25 @@
+odoo.define('mail.FormView', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+
+/**
+ * This file is used to add "related_attachment_count" to fieldsInfo so we can fetch its value for the
+ * chatter's attachment button without having it explicitly declared in the form view template.
+ *
+ */
+
+FormView.include({
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ if ('message_ids' in this.fieldsInfo[this.viewType]) {
+ this.fieldsInfo[this.viewType].related_attachment_count = {};
+ }
+ },
+});
+
+});
diff --git a/addons/mail/static/src/scss/attachment_box.scss b/addons/mail/static/src/scss/attachment_box.scss
new file mode 100644
index 00000000000..ab412bf52fe
--- /dev/null
+++ b/addons/mail/static/src/scss/attachment_box.scss
@@ -0,0 +1,26 @@
+.oe_chatter {
+ .btn.o_active_attach {
+ color: #875A7B;
+ height: 300px;
+ }
+ .o_chatter_attachment {
+ padding: 3px;
+
+ .o_border_dashed {
+ position: relative;
+ margin-top: 15px;
+ margin-bottom: 30px;
+ border-bottom: 1px dashed gray('300');
+ text-align: center;
+
+ .o_attach_title {
+ position: relative;
+ top: 10px;
+ margin: 0 auto;
+ padding: 0 10px;
+ font-weight: bold;
+ background: white;
+ }
+ }
+ }
+}
diff --git a/addons/mail/static/src/scss/chatter.scss b/addons/mail/static/src/scss/chatter.scss
index 01a781a29d4..261552c2b23 100644
--- a/addons/mail/static/src/scss/chatter.scss
+++ b/addons/mail/static/src/scss/chatter.scss
@@ -16,20 +16,30 @@
padding: 0;
display: flex;
+ > .o_topbar_right_area {
+ border-bottom: 1px solid transparent;
+ position: relative;
+ float: right;
+ flex: 1 1 auto;
+ .o_chatter_button_attachment {
+ height: 2.3em;
+ float: right;
+ }
+ .o_followers {
+ height: $o-statusbar-height + 1;
+ border-bottom: 1px solid transparent;
+ flex: 0 1 auto;
+ }
+ }
> .btn {
height: $o-statusbar-height + 1;
border: 1px solid transparent;
}
- > .o_followers {
- height: $o-statusbar-height + 1;
- border-bottom: 1px solid transparent;
- flex: 1 1 auto;
- }
}
&.o_chatter_composer_active .o_chatter_topbar {
- > .btn, > .o_followers{
+ > .btn, > .o_followers, > .o_topbar_right_area{
border-radius: $border-radius-sm $border-radius-sm 0 0;
border-width: 0 0 1px;
border-bottom-color: gray('300');
diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml
index f78f60791e0..7881f3fd49c 100644
--- a/addons/mail/static/src/xml/chatter.xml
+++ b/addons/mail/static/src/xml/chatter.xml
@@ -1,11 +1,34 @@
+
+
+
+
+
+ Attachments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -69,6 +92,18 @@
+
+
+
+
+
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 37fb75572c3..4e1c782a715 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -1,6 +1,7 @@
odoo.define('mail.chatter_tests', function (require) {
"use strict";
+var AttachmentBox = require('mail.AttachmentBox');
var mailTestUtils = require('mail.testUtils');
var concurrency = require('web.concurrency');
@@ -34,7 +35,7 @@ QUnit.module('Chatter', {
string: "Followers",
type: "one2many",
relation: 'mail.followers',
- relation_field: "res_id"
+ relation_field: "res_id",
},
message_ids: {
string: "messages",
@@ -53,9 +54,14 @@ QUnit.module('Chatter', {
type: 'selection',
selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
},
+ related_attachment_count: {
+ string: 'Attachment count',
+ type: 'integer',
+ },
},
records: [{
id: 2,
+ related_attachment_count: 3,
display_name: "first partner",
foo: "HELLO",
message_follower_ids: [],
@@ -134,6 +140,25 @@ QUnit.module('Chatter', {
},
records: [],
},
+ 'ir.attachment': {
+ fields:{
+ name:{type:'char', string:"attachment name", required:true},
+ res_model:{type:'char', string:"res model"},
+ res_id:{type:'integer', string:"res id"},
+ url:{type:'char', string:'url'},
+ type:{ type:'selection', selection:[['url',"URL"],['binary',"BINARY"]]},
+ mimetype:{type:'char', string:"mimetype"},
+ datas_fname:{type:'char', string:"filename"},
+ },
+ records:[
+ {id:1, name:"name1", type:'url', mimetype:'image/png', datas_fname:'filename.jpg',
+ res_id: 7, res_model: 'partner'},
+ {id:2, name:"name2", type:'binary', mimetype:"application/x-msdos-program",
+ datas_fname:"file2.txt", res_id: 7, res_model: 'partner'},
+ {id:3, name:"name2", type:'binary', mimetype:"application/x-msdos-program",
+ datas_fname:"file2.txt", res_id: 5, res_model: 'partner'},
+ ],
+ },
};
},
afterEach: function () {
@@ -144,7 +169,7 @@ QUnit.module('Chatter', {
});
QUnit.test('basic rendering', function (assert) {
- assert.expect(8);
+ assert.expect(9);
var count = 0;
var unwanted_read_count = 0;
@@ -187,6 +212,7 @@ QUnit.test('basic rendering', function (assert) {
"there should be a followers widget, moved inside the chatter's topbar");
assert.ok(form.$('.o_chatter').length, "there should be a chatter widget");
assert.ok(form.$('.o_mail_thread').length, "there should be a mail thread");
+ assert.strictEqual(form.$('.o_chatter_button_attachment').length, 1, "should have one attachment button");
assert.ok(!form.$('.o_chatter_topbar .o_chatter_button_log_note').length,
"log note button should not be available");
@@ -196,6 +222,53 @@ QUnit.test('basic rendering', function (assert) {
form.destroy();
});
+QUnit.test('attachmentBox basic rendering', function (assert) {
+ assert.expect(11);
+ this.data.partner.records.push({
+ id: 7,
+ display_name: "attachment_test",
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ services: this.services,
+ arch: '',
+ res_id: 7,
+ });
+ var $button = form.$('.o_chatter_button_attachment');
+ assert.strictEqual($button.length, 1, "should have one attachment button");
+ $button.click();
+ assert.strictEqual(form.$('.o_mail_chatter_attachments').length, 1,
+ "attachment widget should exist after a first click on the button");
+ assert.strictEqual(form.$('.o_attachment_image').length, 1, "there should be an image preview");
+ assert.strictEqual(form.$('.o_attachments_previews').length, 1, "there should be a list of previews");
+ assert.strictEqual(form.$('.o_attachments_list').length, 1, "there should be a list of non previewable attachments");
+ assert.strictEqual(form.$('.o_attachment_title').text(), 'name1',
+ "the image name should be correct");
+ // since there are two elements "Download name2"; one "name" and the other "txt" as text content, the following test
+ // asserts both at the same time.
+ assert.strictEqual(form.$('a[title = "Download name2"]').text().trim(), 'name2txt',
+ "the attachment name should be correct");
+ assert.ok(form.$('.o_attachment_image').css('background-image').indexOf('/web/image/1/160x160/?crop=true') >= 0,
+ "the attachment image URL should be correct");
+ assert.strictEqual(form.$('.o_attachment_download').eq(0).attr('href'), '/web/content/1?download=true',
+ "the download URL of name1 must be correct");
+ assert.strictEqual(form.$('.o_attachment_download').eq(1).attr('href'), '/web/content/2?download=true',
+ "the download URL of name2 must be correct");
+ $button.click();
+ assert.strictEqual(form.$('.o_mail_chatter_attachments').length, 0,
+ "attachment widget should de destroyed after we reclick the button");
+ form.destroy();
+});
+
QUnit.test('chatter in create mode', function (assert) {
assert.expect(9);
@@ -1496,7 +1569,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) {
} else if (route === '/web/dataset/call_kw/partner/read') {
nbReads++;
if (nbReads === 1) { // first read
- assert.strictEqual(args.args[1].length, 4, 'should read all fiels the first time');
+ assert.strictEqual(args.args[1].length, 5, 'should read all fiels the first time');
} else if (nbReads === 2) { // second read: after the unlink
assert.ok(_.isEqual(args.args[1], ['activity_ids', 'display_name']),
'should only read the activities (+ display_name) after an unlink');
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index 7e116200f43..6a44fb77f4f 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -55,23 +55,26 @@
+
-
-
-
+
+
+
-
-
+
+
+
+
@@ -87,6 +90,7 @@
+