m2m
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Attachment Preview',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Services/Tools',
|
||||
'author': 'Odox SoftHub LLP, Your Name',
|
||||
'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': [
|
||||
'web',
|
||||
'sale', # <-- اعتمادية جديدة ومهمة
|
||||
'size_restriction_for_attachments'
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/sale_menu_attachment_demo.xml', # <-- ملف الواجهة الجديد
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.css',
|
||||
'odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.css',
|
||||
'odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.xml',
|
||||
'odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.xml',
|
||||
'odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.js',
|
||||
'odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
'application': True,
|
||||
'installable': True,
|
||||
'images': ['static/description/thumbnail2.gif'],
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import ir_attachment_Ext
|
||||
from . import attachment_demo
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class AttachmentDemo(models.Model):
|
||||
_name = 'attachment.demo'
|
||||
_description = 'Attachment Preview Demo'
|
||||
|
||||
name = fields.Char(string='Demo Name', required=True)
|
||||
attachment_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
string='Attachments (Preview Widget)',
|
||||
help="Attachments with the custom preview widget."
|
||||
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, api, _
|
||||
from collections import defaultdict
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
from odoo import api, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
@api.model
|
||||
def check(self, mode, values=None):
|
||||
"""Restricts access to ir.attachment according to mode"""
|
||||
if self.env.is_superuser():
|
||||
return True
|
||||
|
||||
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."))
|
||||
|
||||
model_ids = defaultdict(set)
|
||||
if self:
|
||||
self.env['ir.attachment'].flush_model(['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'])
|
||||
|
||||
for res_model, res_ids in model_ids.items():
|
||||
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]:
|
||||
continue
|
||||
records = self.env[res_model].browse(res_ids).exists()
|
||||
access_mode = 'write' if mode in ('create', 'unlink') else mode
|
||||
|
||||
try:
|
||||
records.check_access(access_mode)
|
||||
except AttributeError:
|
||||
records.check_access_rights(access_mode)
|
||||
|
||||
@api.model
|
||||
def read_as_sudo(self, domain=None, fields=None):
|
||||
"""Read attachments with sudo for preview widget"""
|
||||
return self.sudo().search_read(domain or [], fields or [])
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_attachment_demo_user,attachment.demo user,model_attachment_demo,base.group_user,1,1,1,1
|
||||
|
|
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,285 @@
|
|||
/* Modal Full Screen */
|
||||
/* Modal Full Screen - يغطي كل شيء */
|
||||
.o_modal_fullscreen {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
background: rgba(0, 0, 0, 0.75) !important;
|
||||
z-index: 99999 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.o_document_viewer {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.o_viewer_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 30px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.o_viewer_toolbar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-group {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-link {
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-link:hover {
|
||||
color: #ffc107;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.o_close_btn {
|
||||
color: white !important;
|
||||
padding: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.o_close_btn:hover {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.o_viewer_content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_viewer_zoomer {
|
||||
transition: transform 0.1s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_viewer_img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.o_viewer_img:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.o_loading_img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.o_viewer_pdf,
|
||||
.o_viewer_text {
|
||||
width: 95vw;
|
||||
height: 85vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.o_viewer_video {
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
/* Binary Files */
|
||||
.o_viewer_binary {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 60px 40px;
|
||||
}
|
||||
|
||||
.o_binary_icon {
|
||||
margin-bottom: 40px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.o_binary_icon i {
|
||||
font-size: 150px !important;
|
||||
opacity: 0.9;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.o_binary_filename {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
margin: 30px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.o_binary_info {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin: 20px 0 40px;
|
||||
}
|
||||
|
||||
.o_viewer_binary .btn {
|
||||
padding: 18px 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.o_viewer_binary .btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(40, 167, 69, 0.5);
|
||||
}
|
||||
|
||||
/* Navigation Arrows */
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 30px 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
z-index: 10000;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.o_viewer_header {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-link {
|
||||
font-size: 14px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
.o_viewer_pdf {
|
||||
width: 95vw !important;
|
||||
height: 85vh !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5) !important;
|
||||
transition: transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* تحسين Header */
|
||||
.o_viewer_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 30px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.o_viewer_title_section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.o_viewer_filename {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-group {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-group:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-link {
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.o_viewer_toolbar .btn-link:hover {
|
||||
color: #ffc107;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.o_close_btn {
|
||||
color: white !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.o_close_btn:hover {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
const { Component, useState, onMounted, onWillUnmount } = owl;
|
||||
|
||||
export class DocumentViewer extends Component {
|
||||
static template = "odx_m2m_attachment_preview.DocumentViewer";
|
||||
static props = {
|
||||
attachments: { type: Array },
|
||||
activeAttachmentId: { type: Number },
|
||||
close: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
activeId: this.props.activeAttachmentId,
|
||||
scale: 1,
|
||||
angle: 0,
|
||||
enableDrag: false,
|
||||
dragStartX: 0,
|
||||
dragStartY: 0,
|
||||
dragStopX: 0,
|
||||
dragStopY: 0,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
document.addEventListener('keyup', this._onKeyUp);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
document.removeEventListener('keyup', this._onKeyUp);
|
||||
});
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._onKeyUp = this._onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
get activeAttachment() {
|
||||
const attachment = this.props.attachments.find(att => att.id === this.state.activeId);
|
||||
if (attachment && !attachment.fileType) {
|
||||
attachment.fileType = this.getFileType(attachment.mimetype);
|
||||
}
|
||||
return attachment;
|
||||
}
|
||||
|
||||
get transformStyle() {
|
||||
const { scale, angle } = this.state;
|
||||
return `transform: scale3d(${scale}, ${scale}, 1) rotate(${angle}deg);`;
|
||||
}
|
||||
|
||||
get zoomerTransform() {
|
||||
const { dragStopX, dragStopY } = this.state;
|
||||
return `transform: translate3d(${dragStopX}px, ${dragStopY}px, 0);`;
|
||||
}
|
||||
|
||||
getFileType(mimetype) {
|
||||
if (!mimetype) return 'binary';
|
||||
if (mimetype.match(/image/)) return 'image';
|
||||
if (mimetype === 'application/pdf') return 'pdf';
|
||||
if (mimetype.match(/video/)) return 'video';
|
||||
if (mimetype.match(/text/)) return 'text';
|
||||
return 'binary';
|
||||
}
|
||||
|
||||
onNext(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const index = this.props.attachments.findIndex(att => att.id === this.state.activeId);
|
||||
const nextIndex = (index + 1) % this.props.attachments.length;
|
||||
this.state.activeId = this.props.attachments[nextIndex].id;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onPrevious(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const index = this.props.attachments.findIndex(att => att.id === this.state.activeId);
|
||||
const prevIndex = index === 0 ? this.props.attachments.length - 1 : index - 1;
|
||||
this.state.activeId = this.props.attachments[prevIndex].id;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onZoomIn(ev) {
|
||||
ev.stopPropagation();
|
||||
this.state.scale = Math.min(this.state.scale + 0.5, 10);
|
||||
}
|
||||
|
||||
onZoomOut(ev) {
|
||||
ev.stopPropagation();
|
||||
this.state.scale = Math.max(this.state.scale - 0.5, 0.5);
|
||||
}
|
||||
|
||||
onRotate(ev) {
|
||||
ev.stopPropagation();
|
||||
this.state.angle = (this.state.angle + 90) % 360;
|
||||
}
|
||||
|
||||
onZoomReset(ev) {
|
||||
ev.stopPropagation();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.state.scale = 1;
|
||||
this.state.angle = 0;
|
||||
this.state.dragStopX = 0;
|
||||
this.state.dragStopY = 0;
|
||||
}
|
||||
|
||||
onStartDrag(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.state.enableDrag = true;
|
||||
this.state.dragStartX = ev.clientX - this.state.dragStopX;
|
||||
this.state.dragStartY = ev.clientY - this.state.dragStopY;
|
||||
}
|
||||
|
||||
onDrag(ev) {
|
||||
if (this.state.enableDrag) {
|
||||
ev.preventDefault();
|
||||
this.state.dragStopX = ev.clientX - this.state.dragStartX;
|
||||
this.state.dragStopY = ev.clientY - this.state.dragStartY;
|
||||
}
|
||||
}
|
||||
|
||||
onEndDrag(ev) {
|
||||
if (this.state.enableDrag) {
|
||||
ev.preventDefault();
|
||||
this.state.enableDrag = false;
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const delta = ev.deltaY || ev.detail || ev.wheelDelta;
|
||||
if (delta > 0) {
|
||||
this.state.scale = Math.max(this.state.scale - 0.1, 0.5);
|
||||
} else {
|
||||
this.state.scale = Math.min(this.state.scale + 0.1, 10);
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyDown(ev) {
|
||||
if (ev.key === 'ArrowRight') {
|
||||
ev.preventDefault();
|
||||
this.onNext(ev);
|
||||
} else if (ev.key === 'ArrowLeft') {
|
||||
ev.preventDefault();
|
||||
this.onPrevious(ev);
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUp(ev) {
|
||||
if (ev.key === 'Escape') {
|
||||
ev.preventDefault();
|
||||
this.onClose(ev);
|
||||
}
|
||||
}
|
||||
|
||||
onDownload(ev) {
|
||||
ev.stopPropagation();
|
||||
const url = `/web/content/ir.attachment/${this.activeAttachment.id}/datas?download=true`;
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
onClose(ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
if (this.props.close) {
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
||||
onImageClick(ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
onPrint(ev) {
|
||||
ev.stopPropagation();
|
||||
const src = `/web/image/ir.attachment/${this.activeAttachment.id}/datas`;
|
||||
const win = window.open(src);
|
||||
win.print();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odx_m2m_attachment_preview.DocumentViewer" owl="1">
|
||||
<!-- Full Screen Modal -->
|
||||
<div class="o_modal_fullscreen o_document_viewer"
|
||||
t-on-mousemove="onDrag"
|
||||
t-on-mouseup="onEndDrag"
|
||||
t-on-click.self="onClose">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="o_viewer_header">
|
||||
<div class="o_viewer_img_wrapper">
|
||||
<button type="button" class="btn btn-link o_close_btn" t-on-click="onClose" title="Close (ESC)">
|
||||
<i class="fa fa-times fa-2x"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="o_viewer_toolbar">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-link o_zoom_in" t-on-click="onZoomIn" title="Zoom In">
|
||||
<i class="fa fa-search-plus"/>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link o_zoom_reset" t-on-click="onZoomReset" title="Reset">
|
||||
<i class="fa fa-search"/>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link o_zoom_out" t-on-click="onZoomOut" title="Zoom Out">
|
||||
<i class="fa fa-search-minus"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-link o_rotate" t-on-click="onRotate" title="Rotate">
|
||||
<i class="fa fa-repeat"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-link o_print_btn" t-on-click="onPrint" title="Print">
|
||||
<i class="fa fa-print"/>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link o_download_btn" t-on-click="onDownload" title="Download">
|
||||
<i class="fa fa-download"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="o_viewer_content"
|
||||
t-ref="content"
|
||||
t-on-wheel="onScroll">
|
||||
|
||||
<div class="o_viewer_zoomer" t-att-style="zoomerTransform">
|
||||
|
||||
<!-- Image -->
|
||||
<t t-if="activeAttachment.fileType === 'image'">
|
||||
<img class="o_viewer_img"
|
||||
t-att-style="transformStyle"
|
||||
t-att-src="'/web/image/ir.attachment/' + activeAttachment.id + '/datas'"
|
||||
t-on-mousedown.prevent="onStartDrag"
|
||||
t-on-click="onImageClick"
|
||||
alt="Image"/>
|
||||
<div class="o_loading_img">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- PDF -->
|
||||
<t t-if="activeAttachment.fileType === 'pdf'">
|
||||
<iframe class="o_viewer_pdf"
|
||||
t-att-src="'/web/static/lib/pdfjs/web/viewer.html?file=/web/content/ir.attachment/' + activeAttachment.id + '/datas'"/>
|
||||
</t>
|
||||
|
||||
<!-- Video -->
|
||||
<t t-if="activeAttachment.fileType === 'video'">
|
||||
<video class="o_viewer_video" controls="controls" t-on-click="onImageClick">
|
||||
<source t-att-src="'/web/content/ir.attachment/' + activeAttachment.id + '/datas'"/>
|
||||
</video>
|
||||
</t>
|
||||
|
||||
<!-- Text Files -->
|
||||
<t t-if="activeAttachment.fileType === 'text'">
|
||||
<iframe class="o_viewer_text"
|
||||
t-att-src="'/web/content/ir.attachment/' + activeAttachment.id + '/datas'"/>
|
||||
</t>
|
||||
|
||||
<!-- Other Binary Files -->
|
||||
<t t-if="activeAttachment.fileType === 'binary'">
|
||||
<div class="o_viewer_binary">
|
||||
<div class="o_binary_icon">
|
||||
<i t-if="activeAttachment.mimetype && activeAttachment.mimetype.includes('word')"
|
||||
class="fa fa-file-word-o fa-5x text-primary"/>
|
||||
<i t-elif="activeAttachment.mimetype && activeAttachment.mimetype.includes('excel')"
|
||||
class="fa fa-file-excel-o fa-5x text-success"/>
|
||||
<i t-elif="activeAttachment.mimetype && activeAttachment.mimetype.includes('powerpoint')"
|
||||
class="fa fa-file-powerpoint-o fa-5x text-warning"/>
|
||||
<i t-elif="activeAttachment.mimetype && activeAttachment.mimetype.includes('zip')"
|
||||
class="fa fa-file-archive-o fa-5x text-info"/>
|
||||
<i t-else="" class="fa fa-file-o fa-5x text-secondary"/>
|
||||
</div>
|
||||
<h3 class="o_binary_filename"><t t-esc="activeAttachment.name"/></h3>
|
||||
<p class="o_binary_info">
|
||||
<t t-if="activeAttachment.mimetype">
|
||||
Type: <span t-esc="activeAttachment.mimetype"/>
|
||||
</t>
|
||||
</p>
|
||||
<button type="button" class="btn btn-primary btn-lg" t-on-click="onDownload">
|
||||
<i class="fa fa-download"/> Download File
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Arrows -->
|
||||
<t t-if="props.attachments.length > 1">
|
||||
<button type="button" class="arrow arrow-left move_previous" t-on-click="onPrevious" title="Previous (←)">
|
||||
<span class="fa fa-chevron-left fa-2x"/>
|
||||
</button>
|
||||
<button type="button" class="arrow arrow-right move_next" t-on-click="onNext" title="Next (→)">
|
||||
<span class="fa fa-chevron-right fa-2x"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
.o_field_many2many_attachment_preview {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.o_attachments_actions {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.o_attach_btn {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.o_attachments_list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_attachment {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
min-height: 180px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_attachment:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.o_attachment_wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.o_image_box {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.o_image_box .o_image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.o_image_box .o_file_icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.o_image_box .o_file_icon i {
|
||||
font-size: 48px !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.caption {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_attachment_name {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
max-height: 2.8em;
|
||||
}
|
||||
|
||||
.o_attachment_delete {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.o_attachment:hover .o_attachment_delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_attachment_delete:hover {
|
||||
background: #c82333;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.o_attachment {
|
||||
width: 120px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.o_image_box {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { DocumentViewer } from "../document_viewer/document_viewer";
|
||||
|
||||
const { Component, useState, onWillUpdateProps, useRef } = owl;
|
||||
|
||||
export class Many2ManyAttachmentPreview extends Component {
|
||||
static template = "odx_m2m_attachment_preview.Many2ManyAttachmentPreview";
|
||||
static components = { DocumentViewer };
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
|
||||
this.state = useState({
|
||||
attachments: [],
|
||||
showViewer: false,
|
||||
activeAttachmentId: null,
|
||||
});
|
||||
|
||||
this.fileInput = useRef("fileInput");
|
||||
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
const records = nextProps.record.data[this.props.name]?.records;
|
||||
await this.loadAttachments(records);
|
||||
});
|
||||
|
||||
const initialRecords = this.props.record.data[this.props.name]?.records;
|
||||
this.loadAttachments(initialRecords);
|
||||
}
|
||||
|
||||
async loadAttachments(records) {
|
||||
if (!records || !Array.isArray(records) || records.length === 0) {
|
||||
this.state.attachments = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// الحل: استخراج الـ resId الصحيح من السجلات
|
||||
const recordIds = records
|
||||
.map(r => {
|
||||
// إذا كان r عبارة عن object مع resId
|
||||
if (r && typeof r === 'object' && r.resId) {
|
||||
return r.resId;
|
||||
}
|
||||
// إذا كان r عبارة عن object مع id
|
||||
if (r && typeof r === 'object' && r.id) {
|
||||
return r.id;
|
||||
}
|
||||
// إذا كان r رقم مباشرة
|
||||
if (typeof r === 'number') {
|
||||
return r;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(id => id !== null && typeof id === 'number'); // فلترة الأرقام الصحيحة فقط
|
||||
|
||||
if (recordIds.length === 0) {
|
||||
this.state.attachments = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchedAttachments = await this.orm.read(
|
||||
"ir.attachment",
|
||||
recordIds,
|
||||
["id", "name", "mimetype"]
|
||||
);
|
||||
this.state.attachments = fetchedAttachments.map(att => ({
|
||||
...att,
|
||||
fileType: this.getFileType(att.mimetype)
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error loading attachments:", error);
|
||||
this.state.attachments = [];
|
||||
}
|
||||
}
|
||||
|
||||
onAttachClick() {
|
||||
if (!this.props.readonly) {
|
||||
this.fileInput.el.click();
|
||||
}
|
||||
}
|
||||
|
||||
async onFileChange(ev) {
|
||||
const files = ev.target.files;
|
||||
if (!files.length) return;
|
||||
|
||||
if (!this.props.record.resId) {
|
||||
this.notification.add(_t("Please save the record before adding attachments."), { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRecordData = this.props.record.data[this.props.name];
|
||||
|
||||
// استخراج IDs الحالية بشكل صحيح
|
||||
let attachment_ids = [];
|
||||
if (currentRecordData && currentRecordData.records) {
|
||||
attachment_ids = currentRecordData.records
|
||||
.map(r => {
|
||||
if (r && typeof r === 'object' && r.resId) return r.resId;
|
||||
if (r && typeof r === 'object' && r.id) return r.id;
|
||||
if (typeof r === 'number') return r;
|
||||
return null;
|
||||
})
|
||||
.filter(id => id !== null && typeof id === 'number');
|
||||
}
|
||||
|
||||
console.log("Current attachment IDs:", attachment_ids);
|
||||
|
||||
// رفع الملفات الجديدة
|
||||
for (const file of files) {
|
||||
const fileData = await this.readFileAsB64(file);
|
||||
const newAttachmentId = await this.orm.create("ir.attachment", [{
|
||||
name: file.name,
|
||||
datas: fileData,
|
||||
res_model: this.props.record.resModel,
|
||||
res_id: this.props.record.resId,
|
||||
}]);
|
||||
attachment_ids.push(newAttachmentId[0]);
|
||||
}
|
||||
|
||||
console.log("After upload - attachment IDs:", attachment_ids);
|
||||
|
||||
// تحديث الحقل بالطريقة الصحيحة
|
||||
await this.props.record.update({
|
||||
[this.props.name]: [[6, 0, attachment_ids]]
|
||||
});
|
||||
|
||||
// إعادة تحميل المرفقات
|
||||
await this.loadAttachments(attachment_ids.map(id => ({ resId: id })));
|
||||
|
||||
// حفظ السجل
|
||||
try {
|
||||
await this.props.record.model.root.save();
|
||||
console.log("Save successful!");
|
||||
this.notification.add(_t("Attachment(s) uploaded successfully"), { type: "success" });
|
||||
} catch (error) {
|
||||
console.error("Save error:", error);
|
||||
this.notification.add(_t("Failed to save. Please save the record manually."), { type: "warning" });
|
||||
}
|
||||
|
||||
ev.target.value = "";
|
||||
}
|
||||
|
||||
readFileAsB64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result.split(',')[1]);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async onDeleteAttachment(attachmentId) {
|
||||
const currentRecordData = this.props.record.data[this.props.name];
|
||||
|
||||
// استخراج IDs بشكل صحيح
|
||||
let currentIds = [];
|
||||
if (currentRecordData && currentRecordData.records) {
|
||||
currentIds = currentRecordData.records
|
||||
.map(r => {
|
||||
if (r && typeof r === 'object' && r.resId) return r.resId;
|
||||
if (r && typeof r === 'object' && r.id) return r.id;
|
||||
if (typeof r === 'number') return r;
|
||||
return null;
|
||||
})
|
||||
.filter(id => id !== null && typeof id === 'number');
|
||||
}
|
||||
|
||||
const newIds = currentIds.filter(id => id !== attachmentId);
|
||||
|
||||
await this.props.record.update({
|
||||
[this.props.name]: [[6, 0, newIds]]
|
||||
});
|
||||
|
||||
try {
|
||||
await this.props.record.model.root.save();
|
||||
this.notification.add(_t("Attachment removed"), { type: "success" });
|
||||
} catch (error) {
|
||||
console.error("Delete error:", error);
|
||||
this.notification.add(_t("Failed to save. Please save the record manually."), { type: "warning" });
|
||||
}
|
||||
}
|
||||
|
||||
onPreviewAttachment(attachment) {
|
||||
this.state.showViewer = true;
|
||||
this.state.activeAttachmentId = attachment.id;
|
||||
}
|
||||
|
||||
onCloseViewer() {
|
||||
this.state.showViewer = false;
|
||||
this.state.activeAttachmentId = null;
|
||||
}
|
||||
|
||||
getFileType(mimetype) {
|
||||
if (!mimetype) return 'binary';
|
||||
if (mimetype.startsWith('image')) return 'image';
|
||||
if (mimetype === 'application/pdf') return 'pdf';
|
||||
if (mimetype.startsWith('video')) return 'video';
|
||||
return 'binary';
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("many2many_attachment_preview", {
|
||||
component: Many2ManyAttachmentPreview,
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odx_m2m_attachment_preview.Many2ManyAttachmentPreview" owl="1">
|
||||
<div class="o_field_many2many_attachment_preview">
|
||||
<div class="o_attachments_actions">
|
||||
<button type="button" class="btn btn-primary o_attach_btn" t-on-click="onAttachClick">
|
||||
<i class="fa fa-paperclip"/> Attach File(s)
|
||||
</button>
|
||||
<input type="file" class="o_input_file" t-ref="fileInput" t-on-change="onFileChange" multiple="true" style="display: none;"/>
|
||||
</div>
|
||||
|
||||
<div class="o_attachments_list">
|
||||
<t t-if="state.attachments && Array.isArray(state.attachments)">
|
||||
<t t-foreach="state.attachments" t-as="attachment" t-key="attachment.id">
|
||||
<div class="o_attachment" t-att-title="attachment.name">
|
||||
<div class="o_attachment_wrap" t-on-click="() => this.onPreviewAttachment(attachment)">
|
||||
<div class="o_image_box">
|
||||
<img t-if="attachment.mimetype && attachment.mimetype.startsWith('image')"
|
||||
class="o_image"
|
||||
t-att-src="'/web/image/ir.attachment/' + attachment.id + '/datas?width=100&height=100'"/>
|
||||
<div t-else="" class="o_file_icon">
|
||||
<i t-if="attachment.mimetype === 'application/pdf'" class="fa fa-file-pdf-o fa-3x text-danger"/>
|
||||
<i t-elif="attachment.mimetype && attachment.mimetype.includes('word')" class="fa fa-file-word-o fa-3x text-primary"/>
|
||||
<i t-elif="attachment.mimetype && attachment.mimetype.includes('excel')" class="fa fa-file-excel-o fa-3x text-success"/>
|
||||
<i t-else="" class="fa fa-file-o fa-3x text-secondary"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">
|
||||
<span class="o_attachment_name"><t t-esc="attachment.name"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="!props.readonly" class="o_attachment_delete" t-on-click.stop="() => this.onDeleteAttachment(attachment.id)">
|
||||
<span>×</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Document Viewer as Portal (renders outside DOM hierarchy) -->
|
||||
<t t-if="state.showViewer" t-portal="'body'">
|
||||
<DocumentViewer
|
||||
attachments="state.attachments"
|
||||
activeAttachmentId="state.activeAttachmentId"
|
||||
close="() => this.onCloseViewer()"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View -->
|
||||
<record id="view_attachment_demo_form" model="ir.ui.view">
|
||||
<field name="name">attachment.demo.form</field>
|
||||
<field name="model">attachment.demo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Attachment Demo">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Attachments">
|
||||
<field name="attachment_ids" widget="many2many_attachment_preview"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="view_attachment_demo_tree" model="ir.ui.view">
|
||||
<field name="name">attachment.demo.tree</field>
|
||||
<field name="model">attachment.demo</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Attachment Demos">
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_attachment_demo" model="ir.actions.act_window">
|
||||
<field name="name">Attachment Demos</field>
|
||||
<field name="res_model">attachment.demo</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem
|
||||
id="menu_attachment_demo_root"
|
||||
name="Attachment Preview Demo"
|
||||
sequence="100"/>
|
||||
<menuitem
|
||||
id="menu_attachment_demo"
|
||||
name="Demos"
|
||||
parent="menu_attachment_demo_root"
|
||||
action="action_attachment_demo"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- This record forces the main menu to be visible to everyone by clearing its associated groups -->
|
||||
<record id="odx_m2m_attachment_preview.menu_attachment_demo_root" model="ir.ui.menu">
|
||||
<field name="groups_id" eval="[(6, 0, [])]"/>
|
||||
</record>
|
||||
|
||||
<record id="odx_m2m_attachment_preview.menu_attachment_demo" model="ir.ui.menu">
|
||||
<field name="groups_id" eval="[(6, 0, [])]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="action_attachment_demo" model="ir.actions.act_window">
|
||||
<field name="name">Attachment Demos</field>
|
||||
<field name="res_model">attachment.demo</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_attachment_demo_in_sales"
|
||||
name="Attachment Demos"
|
||||
parent="sale.sale_menu_root"
|
||||
sequence="99"
|
||||
action="action_attachment_demo"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
|
||||
|
||||
<record id="view_attachment_demo_form_in_sale" model="ir.ui.view">
|
||||
<field name="name">attachment.demo.form</field>
|
||||
<field name="model">attachment.demo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Attachment Demo">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Attachments">
|
||||
<field name="attachment_ids" widget="many2many_attachment_preview"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||