diff --git a/odex30_base/odx_m2m_attachment_preview/__init__.py b/odex30_base/odx_m2m_attachment_preview/__init__.py new file mode 100644 index 0000000..f5ba686 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models \ No newline at end of file diff --git a/odex30_base/odx_m2m_attachment_preview/__manifest__.py b/odex30_base/odx_m2m_attachment_preview/__manifest__.py new file mode 100644 index 0000000..e2a852a --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/__manifest__.py @@ -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'], +} diff --git a/odex30_base/odx_m2m_attachment_preview/models/__init__.py b/odex30_base/odx_m2m_attachment_preview/models/__init__.py new file mode 100644 index 0000000..457e07f --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import ir_attachment_Ext +from . import attachment_demo \ No newline at end of file diff --git a/odex30_base/odx_m2m_attachment_preview/models/attachment_demo.py b/odex30_base/odx_m2m_attachment_preview/models/attachment_demo.py new file mode 100644 index 0000000..c1383b9 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/models/attachment_demo.py @@ -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." + + ) diff --git a/odex30_base/odx_m2m_attachment_preview/models/ir_attachment_Ext.py b/odex30_base/odx_m2m_attachment_preview/models/ir_attachment_Ext.py new file mode 100644 index 0000000..87e483c --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/models/ir_attachment_Ext.py @@ -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 []) diff --git a/odex30_base/odx_m2m_attachment_preview/security/ir.model.access.csv b/odex30_base/odx_m2m_attachment_preview/security/ir.model.access.csv new file mode 100644 index 0000000..366e20a --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/security/ir.model.access.csv @@ -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 diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/icon.png b/odex30_base/odx_m2m_attachment_preview/static/description/icon.png new file mode 100644 index 0000000..1d3f583 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/icon.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/index.html b/odex30_base/odx_m2m_attachment_preview/static/description/index.html new file mode 100644 index 0000000..9abb9a1 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/description/index.html @@ -0,0 +1,139 @@ +
+
+
+
+
+
+

+ Attachment Preview +

+ +
+ +
+
+
+
+
+

+ Features +

+

+

    +
    +
  • + Added new widget 'many2many_attachment_preview'. +
  • + +
  • + User can preview a document without downloading. +
  • +
+

+ +
+
+
+
+
+
+

+ Look how it to use. +

+
+ +
+
+ +
+
+
+
+
+
+
+

+ Preview will be showing on the screen. +

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+
+

+ Help & Support

+
+
+ +
+ + +
+
+ +
+
+ +
+ + Odoxsofthub.com + +
+
+
+ +
+ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/logo.png b/odex30_base/odx_m2m_attachment_preview/static/description/logo.png new file mode 100644 index 0000000..fc3d874 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/logo.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/m2m_py.png b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_py.png new file mode 100644 index 0000000..bbf8c04 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_py.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/m2m_widget.gif b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_widget.gif new file mode 100644 index 0000000..5f4515d Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_widget.gif differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/m2m_xml.png b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_xml.png new file mode 100644 index 0000000..73c3cf6 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/m2m_xml.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/photo.png b/odex30_base/odx_m2m_attachment_preview/static/description/photo.png new file mode 100644 index 0000000..2996c10 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/photo.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/pic2.png b/odex30_base/odx_m2m_attachment_preview/static/description/pic2.png new file mode 100644 index 0000000..ef4b151 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/pic2.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/sale_order_attachment_prev.png b/odex30_base/odx_m2m_attachment_preview/static/description/sale_order_attachment_prev.png new file mode 100644 index 0000000..97fd21f Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/sale_order_attachment_prev.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/thumbnail2.gif b/odex30_base/odx_m2m_attachment_preview/static/description/thumbnail2.gif new file mode 100644 index 0000000..1f0ef80 Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/thumbnail2.gif differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/description/video.png b/odex30_base/odx_m2m_attachment_preview/static/description/video.png new file mode 100644 index 0000000..9d62e9d Binary files /dev/null and b/odex30_base/odx_m2m_attachment_preview/static/description/video.png differ diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.css b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.css new file mode 100644 index 0000000..f4efff8 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.css @@ -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; +} \ No newline at end of file diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.js b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.js new file mode 100644 index 0000000..90f4d21 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.js @@ -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(); + } +} diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.xml b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.xml new file mode 100644 index 0000000..e0b3830 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/document_viewer/document_viewer.xml @@ -0,0 +1,124 @@ + + + + +
+ + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+ + +
+
+
+ + +
+ +
+ + + + Image +
+ +
+
+ + + +