[ADD] odx_m2m_attachment_preview module added
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Attachment Preview',
|
||||
'version': '14.0.1',
|
||||
'category': 'Services/Tools',
|
||||
'author': 'Odox SoftHub LLP',
|
||||
'website': 'https://www.odoxsofthub.com',
|
||||
'support': 'support@odoxsofthub.com',
|
||||
'sequence': 2,
|
||||
'summary': """This module adds a new widget, many2many_attachment_preview, which enables the user to view attachments without downloading them.""",
|
||||
'description': """ User can preview a document without downloading. """,
|
||||
'price': 16,
|
||||
'currency': 'USD',
|
||||
'depends': [],
|
||||
'data': [
|
||||
'views/assets.xml'
|
||||
],
|
||||
|
||||
"qweb": [
|
||||
"static/src/xml/odx_document_viewer_legacy.xml",
|
||||
"static/src/xml/odx_many2many_attachment_preview.xml",
|
||||
],
|
||||
|
||||
'license': 'LGPL-3',
|
||||
'application': True,
|
||||
'installable': True,
|
||||
'images': ['static/description/thumbnail2.gif'],
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import ir_attachment_Ext
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, api, _
|
||||
from collections import defaultdict
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
# override
|
||||
@api.model
|
||||
def check(self, mode, values=None):
|
||||
""" Restricts the access to an ir.attachment, according to referred mode """
|
||||
if self.env.is_superuser():
|
||||
return True
|
||||
# Always require an internal user (aka, employee) to access to a attachment
|
||||
if not (self.env.is_admin() or self.env.user.has_group('base.group_user')):
|
||||
raise AccessError(
|
||||
_("Sorry, you are not allowed to access this document."))
|
||||
# collect the records to check (by model)
|
||||
model_ids = defaultdict(set) # {model_name: set(ids)}
|
||||
if self:
|
||||
# DLE P173: `test_01_portal_attachment`
|
||||
self.env['ir.attachment'].flush(['res_model', 'res_id', 'create_uid', 'public', 'res_field'])
|
||||
self._cr.execute('SELECT res_model, res_id, create_uid, public, res_field FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
|
||||
for res_model, res_id, create_uid, public, res_field in self._cr.fetchall():
|
||||
if public and mode == 'read':
|
||||
continue
|
||||
if not (res_model and res_id):
|
||||
continue
|
||||
model_ids[res_model].add(res_id)
|
||||
if values and values.get('res_model') and values.get('res_id'):
|
||||
model_ids[values['res_model']].add(values['res_id'])
|
||||
|
||||
# check access rights on the records
|
||||
for res_model, res_ids in model_ids.items():
|
||||
# ignore attachments that are not attached to a resource anymore
|
||||
# when checking access rights (resource was deleted but attachment
|
||||
# was not)
|
||||
if res_model not in self.env:
|
||||
continue
|
||||
if res_model == 'res.users' and len(res_ids) == 1 and self.env.uid == list(res_ids)[0]:
|
||||
# by default a user cannot write on itself, despite the list of writeable fields
|
||||
# e.g. in the case of a user inserting an image into his image signature
|
||||
# we need to bypass this check which would needlessly throw us away
|
||||
continue
|
||||
records = self.env[res_model].browse(res_ids).exists()
|
||||
# For related models, check if we can write to the model, as unlinking
|
||||
# and creating attachments can be seen as an update to the model
|
||||
access_mode = 'write' if mode in ('create', 'unlink') else mode
|
||||
records.check_access_rights(access_mode)
|
||||
records.check_access_rule(access_mode)
|
||||
|
||||
@api.model
|
||||
def read_as_sudo(self, domain=None, fields=None):
|
||||
return self.sudo().search_read(domain, fields)
|
||||
|
After Width: | Height: | Size: 142 KiB |
|
|
@ -0,0 +1,139 @@
|
|||
<section class="oe_container">
|
||||
<section class="oe_spaced mw-100 card module-index_mobile_main_section">
|
||||
<div class="container shadow-sm px-5 text-center module-index_mobile_main_section"
|
||||
style="padding-right:0px !important;padding-left:0px !important;">
|
||||
<section class="oe_container mb-4 mt-4">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-size: 26px;line-height: 32px;color: #bdbab3;">
|
||||
Attachment Preview
|
||||
</h3>
|
||||
<span style="font-size: 16px;color: #555555;"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<div class="oe_span12" style="padding: 0px 0px 0px 0px !important;">
|
||||
<h3 class="panel-title" style="font-size: 18px;line-height: 32px;color: #333333;">
|
||||
Features
|
||||
</h3>
|
||||
<p>
|
||||
<ul style="font-size:20px;text-align:left;">
|
||||
<br>
|
||||
<li>
|
||||
Added new widget 'many2many_attachment_preview'.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
User can preview a document without downloading.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<div class="oe_span12" style="padding: 0px 0px 0px 0px !important;">
|
||||
<h3 class="panel-title" style="font-size: 18px;line-height: 32px;color: #333333;">
|
||||
Look how it to use.
|
||||
</h3>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div img-m2m-field"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="m2m_py.png" style="height:100%;">
|
||||
</div>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div img-m2m-field"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="m2m_xml.png" style="height:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<div class="oe_span12" style="padding: 0px 0px 0px 0px !important;">
|
||||
<h3 class="panel-title" style="font-size: 18px;line-height: 32px;color: #333333;">
|
||||
Preview will be showing on the screen.
|
||||
</h3>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="m2m_widget.gif" style="height:100%;">
|
||||
</div>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="sale_order_attachment_prev.png" style="height:100%;">
|
||||
</div>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="pic2.png" style="height:100%;">
|
||||
</div>
|
||||
<div class="oe_demo oe_picture oe_screenshot mobile-index-img-div"
|
||||
style="max-height:100% !important;">
|
||||
<img class="index-image" src="video.png" style="height:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container" style="padding: 2rem 3rem 1rem;margin-top:5px;">
|
||||
|
||||
<section class="oe_container mb-4 mt-4">
|
||||
<div class="panel panel-primary" style="text-align:center;">
|
||||
<a href="https://apps.odoo.com/apps/modules/browse?search=odx" target="_blank">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-size: 26px;line-height: 32px;color: ##0f0b0b;">
|
||||
Other Apps
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="container" style="margin: 5rem auto 2rem; background-color: #fff !important;">
|
||||
<div class="row" style="max-width:1540px;">
|
||||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mb-4">
|
||||
<hr class="position-absolute"
|
||||
style="border: 1px solid #c4c6cc !important; width: 40% !important; z-index: 0 !important;margin-top:40px;">
|
||||
<h2
|
||||
style="font-size: 26px;background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;">
|
||||
Help & Support</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-flex justify-content-center align-items-center"
|
||||
style="max-width:1540px; margin: 0 auto 2rem auto;">
|
||||
|
||||
<div class="col-lg-12" style="padding: 0rem 3rem 2rem; border-radius: 10px;">
|
||||
<div class="row mt-4 d-flex justify-content-center align-items-center">
|
||||
<div class="col-lg-4">
|
||||
<a href="mailto:support@odoxsofthub.com<" target="_blank"
|
||||
class="btn btn-block mb-2 deep_hover"
|
||||
style="text-decoration: none; background-color: #4d4d4d; color: #C5982C; border-radius: 4px;"><i
|
||||
class="fa fa-envelope mr-2"></i>support@odoxsofthub.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container" style="padding: 2rem 3rem 1rem; background-color: #fff !important;">
|
||||
<div class="row" style="max-width:1540px; margin: 0 auto; margin-right: 3rem; ">
|
||||
|
||||
<div class="col-lg-12 d-flex justify-content-center align-items-center">
|
||||
<a href="https://www.odoxsofthub.com" target="_blank">
|
||||
<img src="logo.png" alt="Odoxsofthub.com">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 5.6 MiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -0,0 +1,404 @@
|
|||
odoo.define('odx_m2m_attachment_preview.DocumentViewer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var SCROLL_ZOOM_STEP = 0.1;
|
||||
var ZOOM_STEP = 0.5;
|
||||
|
||||
var DocumentViewer = Widget.extend({
|
||||
template: "odx_DocumentViewer",
|
||||
events: {
|
||||
'click .o_download_btn': '_onDownload',
|
||||
'click .o_viewer_img': '_onImageClicked',
|
||||
'click .o_viewer_video': '_onVideoClicked',
|
||||
'click .move_next': '_onNext',
|
||||
'click .move_previous': '_onPrevious',
|
||||
'click .o_rotate': '_onRotate',
|
||||
'click .o_zoom_in': '_onZoomIn',
|
||||
'click .o_zoom_out': '_onZoomOut',
|
||||
'click .o_zoom_reset': '_onZoomReset',
|
||||
'click .o_close_btn, .o_viewer_img_wrapper': '_onClose',
|
||||
'click .o_print_btn': '_onPrint',
|
||||
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
|
||||
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
|
||||
'keydown': '_onKeydown',
|
||||
'keyup': '_onKeyUp',
|
||||
'mousedown .o_viewer_img': '_onStartDrag',
|
||||
'mousemove .o_viewer_content': '_onDrag',
|
||||
'mouseup .o_viewer_content': '_onEndDrag'
|
||||
},
|
||||
/**
|
||||
* The documentViewer takes an array of objects describing attachments in
|
||||
* argument, and the ID of an active attachment (the one to display first).
|
||||
* Documents that are not of type image or video are filtered out.
|
||||
*
|
||||
* @override
|
||||
* @param {Array<Object>} attachments list of attachments
|
||||
* @param {integer} activeAttachmentID
|
||||
*/
|
||||
init: function (parent, attachments, activeAttachmentID) {
|
||||
this._super.apply(this, arguments);
|
||||
this.attachment = _.filter(attachments, function (attachment) {
|
||||
var match = attachment.type === 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : attachment.mimetype.match("(image|video|application/pdf|text)");
|
||||
if (match) {
|
||||
attachment.fileType = match[1];
|
||||
if (match[1].match("(.png|.jpg|.gif)")) {
|
||||
attachment.fileType = 'image';
|
||||
}
|
||||
if (match[1] === 'youtu') {
|
||||
var youtube_array = attachment.url.split('/');
|
||||
var youtube_token = youtube_array[youtube_array.length - 1];
|
||||
if (youtube_token.indexOf('watch') !== -1) {
|
||||
youtube_token = youtube_token.split('v=')[1];
|
||||
var amp = youtube_token.indexOf('&');
|
||||
if (amp !== -1) {
|
||||
youtube_token = youtube_token.substring(0, amp);
|
||||
}
|
||||
}
|
||||
attachment.youtube = youtube_token;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.activeAttachment = _.findWhere(attachments, { id: activeAttachmentID });
|
||||
this.modelName = 'ir.attachment';
|
||||
this._reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a modal displaying the active attachment
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.modal('show');
|
||||
this.$el.on('hidden.bs.modal', _.bind(this._onDestroy, this));
|
||||
this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this));
|
||||
this.$('[data-toggle="tooltip"]').tooltip({ delay: 0 });
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
this.trigger_up('document_viewer_closed');
|
||||
this.$el.modal('hide');
|
||||
this.$el.remove();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_next: function () {
|
||||
var index = _.findIndex(this.attachment, this.activeAttachment);
|
||||
index = (index + 1) % this.attachment.length;
|
||||
this.activeAttachment = this.attachment[index];
|
||||
this._updateContent();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_previous: function () {
|
||||
var index = _.findIndex(this.attachment, this.activeAttachment);
|
||||
index = index === 0 ? this.attachment.length - 1 : index - 1;
|
||||
this.activeAttachment = this.attachment[index];
|
||||
this._updateContent();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_reset: function () {
|
||||
this.scale = 1;
|
||||
this.dragStartX = this.dragstopX = 0;
|
||||
this.dragStartY = this.dragstopY = 0;
|
||||
},
|
||||
/**
|
||||
* Render the active attachment
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateContent: function () {
|
||||
this.$('.o_viewer_content').html(QWeb.render('odx.DocumentViewer.Content', {
|
||||
widget: this
|
||||
}));
|
||||
this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this));
|
||||
this.$('[data-toggle="tooltip"]').tooltip({ delay: 0 });
|
||||
this._reset();
|
||||
},
|
||||
/**
|
||||
* Get CSS transform property based on scale and angle
|
||||
*
|
||||
* @private
|
||||
* @param {float} scale
|
||||
* @param {float} angle
|
||||
*/
|
||||
_getTransform: function (scale, angle) {
|
||||
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)';
|
||||
},
|
||||
/**
|
||||
* Rotate image clockwise by provided angle
|
||||
*
|
||||
* @private
|
||||
* @param {float} angle
|
||||
*/
|
||||
_rotate: function (angle) {
|
||||
this._reset();
|
||||
var new_angle = (this.angle || 0) + angle;
|
||||
this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle));
|
||||
this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%');
|
||||
this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%');
|
||||
this.angle = new_angle;
|
||||
},
|
||||
/**
|
||||
* Zoom in/out image by provided scale
|
||||
*
|
||||
* @private
|
||||
* @param {integer} scale
|
||||
*/
|
||||
_zoom: function (scale) {
|
||||
if (scale > 0.5) {
|
||||
this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0));
|
||||
this.scale = scale;
|
||||
}
|
||||
this.$('.o_zoom_reset').add('.o_zoom_out').toggleClass('disabled', scale === 1);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onClose: function (e) {
|
||||
e.preventDefault();
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* When popup close complete destroyed modal even DOM footprint too
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onDestroy: function () {
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onDownload: function (e) {
|
||||
e.preventDefault();
|
||||
window.location = '/web/content/' + this.modelName + '/' + this.activeAttachment.id + '/' + 'datas' + '?download=true';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onDrag: function (e) {
|
||||
e.preventDefault();
|
||||
if (this.enableDrag) {
|
||||
var $image = this.$('.o_viewer_img');
|
||||
var $zoomer = this.$('.o_viewer_zoomer');
|
||||
var top = $image.prop('offsetHeight') * this.scale > $zoomer.height() ? e.clientY - this.dragStartY : 0;
|
||||
var left = $image.prop('offsetWidth') * this.scale > $zoomer.width() ? e.clientX - this.dragStartX : 0;
|
||||
$zoomer.css("transform", "translate3d(" + left + "px, " + top + "px, 0)");
|
||||
$image.css('cursor', 'move');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onEndDrag: function (e) {
|
||||
e.preventDefault();
|
||||
if (this.enableDrag) {
|
||||
this.enableDrag = false;
|
||||
this.dragstopX = e.clientX - this.dragStartX;
|
||||
this.dragstopY = e.clientY - this.dragStartY;
|
||||
this.$('.o_viewer_img').css('cursor', '');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On click of image do not close modal so stop event propagation
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onImageClicked: function (e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
/**
|
||||
* Remove loading indicator when image loaded
|
||||
* @private
|
||||
*/
|
||||
_onImageLoaded: function () {
|
||||
this.$('.o_loading_img').hide();
|
||||
},
|
||||
/**
|
||||
* Move next previous attachment on keyboard right left key
|
||||
*
|
||||
* @private
|
||||
* @param {KeyEvent} e
|
||||
*/
|
||||
_onKeydown: function (e) {
|
||||
switch (e.which) {
|
||||
case $.ui.keyCode.RIGHT:
|
||||
e.preventDefault();
|
||||
this._next();
|
||||
break;
|
||||
case $.ui.keyCode.LEFT:
|
||||
e.preventDefault();
|
||||
this._previous();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Close popup on ESCAPE keyup
|
||||
*
|
||||
* @private
|
||||
* @param {KeyEvent} e
|
||||
*/
|
||||
_onKeyUp: function (e) {
|
||||
switch (e.which) {
|
||||
case $.ui.keyCode.ESCAPE:
|
||||
e.preventDefault();
|
||||
this._onClose(e);
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onNext: function (e) {
|
||||
e.preventDefault();
|
||||
this._next();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onPrevious: function (e) {
|
||||
e.preventDefault();
|
||||
this._previous();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onPrint: function (e) {
|
||||
// e.preventDefault();
|
||||
var src = this.$('.o_viewer_img').prop('src');
|
||||
print(src,"srcsrcsrc")
|
||||
var script = QWeb.render('odx.DocumentViewer.Content', {
|
||||
src: src
|
||||
});
|
||||
var printWindow = window.open('about:blank', "_new");
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(script);
|
||||
printWindow.document.close();
|
||||
// e.preventDefault();
|
||||
// var src = this.$('.o_viewer_img').prop('src');
|
||||
// var script = QWeb.render('odx.DocumentViewer.Content', {
|
||||
// src: src
|
||||
// });
|
||||
// var printWindow = window.open('about:blank', "_new");
|
||||
// printWindow.document.open();
|
||||
// printWindow.document.write(script);
|
||||
// printWindow.document.close();
|
||||
// ev.stopPropagation();
|
||||
// this._print();
|
||||
},
|
||||
/**
|
||||
* Zoom image on scroll
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onScroll: function (e) {
|
||||
var scale;
|
||||
if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) {
|
||||
scale = this.scale + SCROLL_ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
} else {
|
||||
scale = this.scale - SCROLL_ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onStartDrag: function (e) {
|
||||
e.preventDefault();
|
||||
this.enableDrag = true;
|
||||
this.dragStartX = e.clientX - (this.dragstopX || 0);
|
||||
this.dragStartY = e.clientY - (this.dragstopY || 0);
|
||||
},
|
||||
/**
|
||||
* On click of video do not close modal so stop event propagation
|
||||
* and provide play/pause the video instead of quitting it
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onVideoClicked: function (e) {
|
||||
e.stopPropagation();
|
||||
var videoElement = e.target;
|
||||
if (videoElement.paused) {
|
||||
videoElement.play();
|
||||
} else {
|
||||
videoElement.pause();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onRotate: function (e) {
|
||||
e.preventDefault();
|
||||
this._rotate(90);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomIn: function (e) {
|
||||
e.preventDefault();
|
||||
var scale = this.scale + ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomOut: function (e) {
|
||||
e.preventDefault();
|
||||
var scale = this.scale - ZOOM_STEP;
|
||||
this._zoom(scale);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onZoomReset: function (e) {
|
||||
e.preventDefault();
|
||||
this.$('.o_viewer_zoomer').css("transform", "");
|
||||
this._zoom(1);
|
||||
},
|
||||
});
|
||||
return DocumentViewer;
|
||||
});
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
odoo.define('odx_m2m_attachment_preview.odx_many2many_attachment_preview', function (require) {
|
||||
"use strict";
|
||||
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var field_registry = require('web.field_registry');
|
||||
var core = require('web.core');
|
||||
var relational_fields = require('web.relational_fields');
|
||||
var DocumentViewer = require('odx_m2m_attachment_preview.DocumentViewer');
|
||||
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
var qweb = core.qweb;
|
||||
/**
|
||||
* Widget to upload or delete one or more files at the same time.
|
||||
*/
|
||||
var FieldMany2ManyAttachmentPreview = AbstractField.extend({
|
||||
|
||||
|
||||
|
||||
template: "FieldBinaryFileUploader",
|
||||
template_files: "FieldAttachmentFileUploader.files",
|
||||
supportedFieldTypes: ['many2many'],
|
||||
fieldsToFetch: {
|
||||
name: {type: 'char'},
|
||||
mimetype: {type: 'char'},
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .o_attach': '_onAttach',
|
||||
'click .o_attachment_delete': '_onDelete',
|
||||
'change .o_input_file': '_onFileChanged',
|
||||
'click .o_attachment_wrap': '_previewAttachment',
|
||||
},
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (this.field.type !== 'many2many' || this.field.relation !== 'ir.attachment') {
|
||||
var msg = _t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model.");
|
||||
throw _.str.sprintf(msg, this.field.string);
|
||||
}
|
||||
|
||||
this.uploadedFiles = {};
|
||||
this.uploadingFiles = [];
|
||||
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
|
||||
this.accepted_file_extensions = (this.nodeOptions && this.nodeOptions.accepted_file_extensions) || this.accepted_file_extensions || '*';
|
||||
$(window).on(this.fileupload_id, this._onFileLoaded.bind(this));
|
||||
|
||||
this.metadata = {};
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this._super();
|
||||
$(window).off(this.fileupload_id);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Compute the URL of an attachment.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} attachment
|
||||
* @returns {string} URL of the attachment
|
||||
*/
|
||||
_getFileUrl: function (attachment) {
|
||||
// return '/web/content/' + attachment.id + '?download=true';
|
||||
return '/web/content/' + attachment.id;
|
||||
},
|
||||
/**
|
||||
* Process the field data to add some information (url, etc.).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_generatedMetadata: function () {
|
||||
var self = this;
|
||||
_.each(this.value.data, function (record) {
|
||||
// tagging `allowUnlink` ascertains if the attachment was user
|
||||
// uploaded or was an existing or system generated attachment
|
||||
self.metadata[record.id] = {
|
||||
allowUnlink: self.uploadedFiles[record.data.id] || false,
|
||||
url: self._getFileUrl(record.data),
|
||||
};
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_render: function () {
|
||||
// render the attachments ; as the attachments will changes after each
|
||||
// _setValue, we put the rendering here to ensure they will be updated
|
||||
|
||||
this._generatedMetadata();
|
||||
this.$('.oe_placeholder_files, .o_attachments')
|
||||
.replaceWith($(qweb.render(this.template_files, {
|
||||
widget: this,
|
||||
})));
|
||||
this.$('.oe_fileupload').show();
|
||||
|
||||
this.$('.o_image[data-mimetype^="image"]').each(function () {
|
||||
var $img = $(this);
|
||||
if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
|
||||
$img.css('background-image', "url('" + $img.data('src') + "')");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAttach: function () {
|
||||
// This widget uses a hidden form to upload files. Clicking on 'Attach'
|
||||
// will simulate a click on the related input.
|
||||
this.$('.o_input_file').click();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onDelete: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
var fileID = $(ev.currentTarget).data('id');
|
||||
var record = _.findWhere(this.value.data, {res_id: fileID});
|
||||
if (record) {
|
||||
this._setValue({
|
||||
operation: 'FORGET',
|
||||
ids: [record.id],
|
||||
});
|
||||
var metadata = this.metadata[record.id];
|
||||
if (!metadata || metadata.allowUnlink) {
|
||||
this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'unlink',
|
||||
args: [record.res_id],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onFileChanged: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
var files = ev.target.files;
|
||||
var attachment_ids = this.value.res_ids;
|
||||
|
||||
// Don't create an attachment if the upload window is cancelled.
|
||||
if(files.length === 0)
|
||||
return;
|
||||
|
||||
_.each(files, function (file) {
|
||||
var record = _.find(self.value.data, function (attachment) {
|
||||
return attachment.data.name === file.name;
|
||||
});
|
||||
if (record) {
|
||||
var metadata = self.metadata[record.id];
|
||||
if (!metadata || metadata.allowUnlink) {
|
||||
// there is a existing attachment with the same name so we
|
||||
// replace it
|
||||
attachment_ids = _.without(attachment_ids, record.res_id);
|
||||
self._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'unlink',
|
||||
args: [record.res_id],
|
||||
});
|
||||
}
|
||||
}
|
||||
self.uploadingFiles.push(file);
|
||||
});
|
||||
|
||||
this._setValue({
|
||||
operation: 'REPLACE_WITH',
|
||||
ids: attachment_ids,
|
||||
});
|
||||
|
||||
this.$('form.o_form_binary_form').submit();
|
||||
this.$('.oe_fileupload').hide();
|
||||
ev.target.value = "";
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onFileLoaded: function () {
|
||||
var self = this;
|
||||
// the first argument isn't a file but the jQuery.Event
|
||||
var files = Array.prototype.slice.call(arguments, 1);
|
||||
// files has been uploaded, clear uploading
|
||||
this.uploadingFiles = [];
|
||||
|
||||
var attachment_ids = this.value.res_ids;
|
||||
_.each(files, function (file) {
|
||||
if (file.error) {
|
||||
self.displayNotification({ title: _t('Uploading Error'), message: file.error, type: 'danger' });
|
||||
} else {
|
||||
attachment_ids.push(file.id);
|
||||
self.uploadedFiles[file.id] = true;
|
||||
}
|
||||
});
|
||||
|
||||
this._setValue({
|
||||
operation: 'REPLACE_WITH',
|
||||
ids: attachment_ids,
|
||||
});
|
||||
},
|
||||
|
||||
_previewAttachment: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var activeAttachmentID = $(ev.currentTarget).data('id');
|
||||
this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'read_as_sudo',
|
||||
kwargs: {
|
||||
domain: [['id', 'in', this.value.res_ids]],
|
||||
fields: ['id', 'mimetype', 'index_content'],
|
||||
},
|
||||
}).then(result => {
|
||||
this.attachments = result.map(r => {
|
||||
return {
|
||||
id: r.id,
|
||||
mimetype: r.mimetype,
|
||||
fileType: r.index_content
|
||||
}
|
||||
});
|
||||
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
|
||||
attachmentViewer.appendTo($('body'));
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
field_registry.add("many2many_attachment_preview", FieldMany2ManyAttachmentPreview);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="odx.DocumentViewer.Content">
|
||||
<div class="o_viewer_content">
|
||||
<t t-set="model" t-value="widget.modelName" />
|
||||
<div class="o_viewer-header">
|
||||
<span class="o_image_caption">
|
||||
<i class="fa fa-picture-o mr8" t-if="widget.activeAttachment.fileType == 'image'" role="img" aria-label="Image" title="Image" />
|
||||
<i class="fa fa-file-text mr8" t-if="widget.activeAttachment.fileType == 'application/pdf'" role="img" aria-label="PDF file" title="PDF file" />
|
||||
<i class="fa fa-video-camera mr8" t-if="widget.activeAttachment.fileType == 'video'" role="img" aria-label="Video" title="Video" />
|
||||
<span class="o_viewer_document_name" t-esc="widget.activeAttachment.name" />
|
||||
</span>
|
||||
<div class="o-autogrow" />
|
||||
<a class="o_download_btn o_document_viewer_topbar_button btn" href="#" title="Download">
|
||||
<i class="fa fa-fw fa-download" role="img" aria-label="Download" />
|
||||
<span class="d-none d-md-inline ml-2">Download</span>
|
||||
</a>
|
||||
<a role="button" href="#" class="o_close_btn o_document_viewer_topbar_button btn" title="Close">
|
||||
<i class="fa fa-fw fa-close" role="img" aria-label="Close" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="o_viewer_img_wrapper">
|
||||
<div class="o_viewer_zoomer">
|
||||
<t t-if="widget.activeAttachment.fileType === 'image'">
|
||||
<div class="o_loading_img text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin text-gray-light fa-3x fa-fw" role="img" aria-label="Loading" title="Loading" />
|
||||
</div>
|
||||
<t t-set="unique" t-value="widget.activeAttachment.checksum ? widget.activeAttachment.checksum.slice(-8) : ''" />
|
||||
<img class="o_viewer_img" t-attf-src="/web/image/#{widget.activeAttachment.id}?unique=#{unique}&model=#{model}" alt="Viewer" />
|
||||
</t>
|
||||
<iframe t-if="widget.activeAttachment.fileType == 'application/pdf'" class="o_viewer_pdf" t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/web/content/#{widget.activeAttachment.id}?model%3D#{model}%26filename%3D#{window.encodeURIComponent(widget.activeAttachment.name)}" />
|
||||
<iframe t-if="(widget.activeAttachment.fileType || '').indexOf('text') !== -1" class="o_viewer_text" t-attf-src="/web/content/#{widget.activeAttachment.id}?model=#{model}" />
|
||||
<iframe t-if="widget.activeAttachment.fileType == 'youtu'" class="o_viewer_text" allow="autoplay; encrypted-media" width="560" height="315" t-attf-src="https://www.youtube.com/embed/#{widget.activeAttachment.youtube}" />
|
||||
<video t-if="widget.activeAttachment.fileType == 'video'" class="o_viewer_video" controls="controls">
|
||||
<source t-attf-src="/web/image/#{widget.activeAttachment.id}?model=#{model}" t-att-data-type="widget.activeAttachment.mimetype" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="widget.activeAttachment.fileType == 'image'" class="o_viewer_toolbar btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_in" data-toggle="tooltip" title="Zoom In">
|
||||
<i class="fa fa-fw fa-plus" role="img" aria-label="Zoom In" />
|
||||
</a>
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_reset disabled" data-toggle="tooltip" title="Reset Zoom">
|
||||
<i class="fa fa-fw fa-search" role="img" aria-label="Reset Zoom" />
|
||||
</a>
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_zoom_out disabled" data-toggle="tooltip" title="Zoom Out">
|
||||
<i class="fa fa-fw fa-minus" role="img" aria-label="Zoom Out" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_rotate" data-toggle="tooltip" title="Rotate">
|
||||
<i class="fa fa-fw fa-repeat" role="img" aria-label="Rotate" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a role="button" href="#" class="o_viewer_toolbar_btn btn o_download_btn" data-toggle="tooltip" title="Download">
|
||||
<i class="fa fa-fw fa-download" role="img" aria-label="Download" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="odx_DocumentViewer">
|
||||
<div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog">
|
||||
<t class="o_document_viewer_content_call" t-call="odx.DocumentViewer.Content" />
|
||||
|
||||
<t t-if="widget.attachment.length !== 1">
|
||||
<a class="arrow arrow-left move_previous" href="#">
|
||||
<span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous" />
|
||||
</a>
|
||||
<a class="arrow arrow-right move_next" href="#">
|
||||
<span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next" />
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="PrintImage">
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
function onload_img() {
|
||||
setTimeout('print_img()', 10);
|
||||
}
|
||||
function print_img() {
|
||||
window.print();
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload='onload_img()'>
|
||||
<img t-att-src='src' alt="" />
|
||||
</body>
|
||||
</html>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="FieldAttachmentFileUploader.attachment_preview">
|
||||
<t t-set="url" t-value="widget.metadata[file.id] ? widget.metadata[file.id].url : false" />
|
||||
<t t-if="file.data" t-set="file" t-value="file.data" />
|
||||
<t t-set="editable" t-value="widget.mode === 'edit'" />
|
||||
<t t-if="file.mimetype" t-set="mimetype" t-value="file.mimetype" />
|
||||
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}" t-att-title="file.name">
|
||||
<div class="o_attachment_wrap" t-att-data-id="file.id" style="cursor: pointer;">
|
||||
<t t-set="ext" t-value="file.name.replace(/^.*\./, '')" />
|
||||
<div class="o_image_box float-left" t-att-data-id="file.id">
|
||||
<a t-att-href="url" t-att-title="'Download ' + file.name" aria-label="Download">
|
||||
<span class="o_image o_hover" t-att-data-mimetype="mimetype" t-att-data-ext="ext" role="img" t-attf-data-src="/web/content/{{file.id}}" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="caption">
|
||||
<a class="ml4" t-att-href="url" t-att-title="'Download ' + file.name">
|
||||
<t t-esc='file.name' />
|
||||
</a>
|
||||
</div>
|
||||
<div class="caption small">
|
||||
<a class="ml4 small text-uppercase" t-att-href="url" t-att-title="'Download ' + file.name">
|
||||
<b>
|
||||
<t t-esc='ext' />
|
||||
</b>
|
||||
</a>
|
||||
<div t-if="editable" class="progress o_attachment_progress_bar">
|
||||
<div class="progress-bar progress-bar-striped active" style="width: 100%">Uploading</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="editable" class="o_attachment_uploaded">
|
||||
<i class="text-success fa fa-check" role="img" aria-label="Uploaded" title="Uploaded" />
|
||||
</div>
|
||||
<div t-if="editable" class="o_attachment_delete" t-att-data-id="file.id">
|
||||
<span class="text-white" role="img" aria-label="Delete" title="Delete">×</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div t-name="FieldAttachmentFileUploader.files" class="o_attachments" aria-atomic="true">
|
||||
<!-- uploaded files -->
|
||||
<t t-foreach="widget.value.data" t-as="file">
|
||||
<t t-if="!file.data.upload" t-call="FieldAttachmentFileUploader.attachment_preview" />
|
||||
</t>
|
||||
<!-- uploading files -->
|
||||
<t t-foreach="widget.uploadingFiles" t-as="file">
|
||||
<t t-set="upload" t-value="true" />
|
||||
<t t-call="FieldAttachmentFileUploader.attachment_preview" />
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="Export Web Backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
|
||||
<script type="text/javascript" src="/odx_m2m_attachment_preview/static/src/js/odx_document_viewer_legacy.js"></script>
|
||||
<script type="text/javascript" src="/odx_m2m_attachment_preview/static/src/js/odx_many2many_attachment_preview.js"></script>
|
||||
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||