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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Other Apps + + + + + + + + + + + + + + Help & Support + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Type: + + + + Download File + + + + + + + + + + + + + + + + + + diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.css b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.css new file mode 100644 index 0000000..6bedc4f --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.css @@ -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; + } +} diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.js b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.js new file mode 100644 index 0000000..efd2cfe --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.js @@ -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, +}); diff --git a/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.xml b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.xml new file mode 100644 index 0000000..be84c7a --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/static/src/components/many2many_attachment_preview/many2many_attachment_preview.xml @@ -0,0 +1,49 @@ + + + + + + + Attach File(s) + + + + + + + + + + + + + + + + + + + + + + + + × + + + + + + + + + + + + + diff --git a/odex30_base/odx_m2m_attachment_preview/views/attachment_demo_views.xml b/odex30_base/odx_m2m_attachment_preview/views/attachment_demo_views.xml new file mode 100644 index 0000000..be50d19 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/views/attachment_demo_views.xml @@ -0,0 +1,51 @@ + + + + + attachment.demo.form + attachment.demo + + + + + + + + + + + + + + + + + + + attachment.demo.tree + attachment.demo + + + + + + + + + + Attachment Demos + attachment.demo + list,form + + + + + + diff --git a/odex30_base/odx_m2m_attachment_preview/views/fix_menu_security.xml b/odex30_base/odx_m2m_attachment_preview/views/fix_menu_security.xml new file mode 100644 index 0000000..6b8d1e2 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/views/fix_menu_security.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/odex30_base/odx_m2m_attachment_preview/views/sale_menu_attachment_demo.xml b/odex30_base/odx_m2m_attachment_preview/views/sale_menu_attachment_demo.xml new file mode 100644 index 0000000..a768dc8 --- /dev/null +++ b/odex30_base/odx_m2m_attachment_preview/views/sale_menu_attachment_demo.xml @@ -0,0 +1,38 @@ + + + + + Attachment Demos + attachment.demo + list,form + + + + + + + attachment.demo.form + attachment.demo + + + + + + + + + + + + + + + +
+
+ + Type: + +