@ -0,0 +1,2 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from . import models |
@ -0,0 +1,100 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################################# |
||||
|
# Author : Expert IT Solutions (<www.expertpk.com>) |
||||
|
# Copyright(c): 2012-Present Expert IT Solutions |
||||
|
# All Rights Reserved. |
||||
|
# |
||||
|
# This program is copyright property of the author mentioned above. |
||||
|
# You can`t redistribute it and/or modify it. |
||||
|
# |
||||
|
################################################################################# |
||||
|
{ |
||||
|
'name': "One2many Image Drag and Drop Widget | Image Drag And Drop Widget ", |
||||
|
'version': '17.0.1.4.1', |
||||
|
'author': "Expert IT Solutions", |
||||
|
'website': "https://www.expertpk.com", |
||||
|
'summary': """ |
||||
|
Two widgets. 1: Binary Image Drag and Drop Widget 2: One2Many Image(s) Field Drag And Drop Widget. |
||||
|
Enhance your Odoo experience with a custom widget for drag and drop image functionality. |
||||
|
Keywords: one2many image drag and drop, image drag and drop, drag & drop, binary field image upload, file uploader, bulk image upload, media management, image preview, product image uploader, one2many drag and drop, o2m drag and drop, binary drag & drop, drag and drop pictures, image widget, zoom image, image zoom, binary fields, image_ids, product_template_image_ids, product_variant_image_ids, multiple images, upload multiple images, one2many widget, o2m widget, o2m uploader upload uploading image images multi media multimedia media owl widget widget view custom widget field widget. |
||||
|
""", |
||||
|
'description': """ |
||||
|
The Drag And Drop Binary Field Widget for Odoo is a cutting-edge proprietary solution designed for advanced media management. |
||||
|
It provides an intuitive drag and drop interface to upload, preview, and manage images efficiently within your Odoo records. |
||||
|
|
||||
|
Key Features and Keywords: |
||||
|
- Image Drag And Drop Widget |
||||
|
- Bulk Image Upload |
||||
|
- Media Management |
||||
|
- File Uploader |
||||
|
- Odoo Binary Field Widget |
||||
|
- Custom Odoo Widget |
||||
|
- E-commerce Image Management |
||||
|
- Real Estate Image Management |
||||
|
- Drag and drop widget for Odoo |
||||
|
- Odoo image upload |
||||
|
- Odoo image preview |
||||
|
- Odoo file uploader |
||||
|
- Drag and drop binary field |
||||
|
- o2m field widget |
||||
|
- one2many field widget |
||||
|
- one2many drag and drop |
||||
|
- one2many image upload |
||||
|
- multiple image upload |
||||
|
- multiple images drag and drop |
||||
|
- drag and drop multiple images |
||||
|
- drag and drop multiple files |
||||
|
- image_1920 field widget |
||||
|
- image.mixin field widget |
||||
|
- product.image field widget |
||||
|
- website product multiple images |
||||
|
- website product drag and drop widget |
||||
|
- website product image upload |
||||
|
- Drag and Drop Interface |
||||
|
- Binary Field Widget |
||||
|
- Image Upload & Preview |
||||
|
- File Uploader & Media Management |
||||
|
- Custom Odoo Widget for E-commerce, Real Estate, Media Portals, and More |
||||
|
- Seamless Integration with Odoo's Backend |
||||
|
- Product Image Uploader, Custom Image Fields, and Dynamic Widgets |
||||
|
|
||||
|
IMPORTANT LICENSE & USAGE TERMS: |
||||
|
- Licensed under Odoo Proprietary License v1 (OPL-1). This license is extremely restrictive. |
||||
|
- Modification, redistribution, or reuse of any part of this module’s code in any other modules or applications is strictly prohibited. |
||||
|
- This module is intended for use on a single database only. For additional databases, buy additional licenses. |
||||
|
- Code modifications, reverse-engineering, or using the code to generate derivative works is not allowed. |
||||
|
- For full details, please refer to the separate LICENSE file provided with the module. |
||||
|
|
||||
|
Perfect for: |
||||
|
- E-commerce websites looking to manage multiple product images effortlessly. |
||||
|
- Businesses with heavy media content, such as real estate, portfolios, or media portals. |
||||
|
- Developers and system integrators seeking to enhance the user experience without extra dependencies, |
||||
|
as the widget is available systemwide once installed. |
||||
|
""", |
||||
|
'license': 'OPL-1', |
||||
|
'category': 'Tools', |
||||
|
'depends': ['base_setup', 'base', 'web', 'product', 'website_sale'], |
||||
|
# uncomment above line to automate the process of product upgrade and comment the below depend line |
||||
|
# 'depends': ['base_setup', 'base', 'web', 'product'], |
||||
|
|
||||
|
'data': [ |
||||
|
'views/product.xml', # uncomment this line only if you added website_sale as dependency. |
||||
|
], |
||||
|
'assets': { |
||||
|
'web.assets_backend': [ |
||||
|
'eis_drag_drop/static/src/css/style.scss', |
||||
|
'eis_drag_drop/static/src/css/styles.css', |
||||
|
'eis_drag_drop/static/src/js/dnd_image_widget.js', |
||||
|
'eis_drag_drop/static/src/js/dnd_images_widget.js', |
||||
|
'eis_drag_drop/static/src/xml/dnd_widgets_templates.xml', |
||||
|
], |
||||
|
}, |
||||
|
'images': ['static/description/banner.gif'], |
||||
|
'live_test_url': 'https://www.youtube.com/watch?v=JRPchiEhS9U', |
||||
|
'price': 70.0, |
||||
|
'currency': 'EUR', |
||||
|
'auto_install': False, |
||||
|
'installable': True, |
||||
|
} |
||||
|
|
||||
|
|
@ -0,0 +1,949 @@ |
|||||
|
.. container:: top-bar d-flex justify-content-between align-items-center |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
|Logo| |
||||
|
|
||||
|
.. container:: availability |
||||
|
|
||||
|
Community Enterprise Odoo Online |
||||
|
|
||||
|
.. container:: section hero-section |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: Images Drag And Drop Widgets |
||||
|
:name: images-drag-and-drop-widgets |
||||
|
|
||||
|
Effortlessly manage and update images in your Odoo records with |
||||
|
our intuitive drag and drop widgets. |
||||
|
|
||||
|
.. container:: vector-placeholder |
||||
|
|
||||
|
|Hero Vector| |
||||
|
|
||||
|
.. container:: benefit-box |
||||
|
|
||||
|
Perfect for E-commerce, Media Portals & More! |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: introduction |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Introduction |
||||
|
:name: introduction |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
The **Images Drag And Drop Widgets** module is a state-of-the-art |
||||
|
solution designed to enhance your Odoo experience by integrating |
||||
|
modern, user-friendly drag and drop image widgets. |
||||
|
|
||||
|
.. rubric:: Who Can Benefit? |
||||
|
:name: who-can-benefit |
||||
|
|
||||
|
Whether you run an e-commerce site, a media portal, a real estate |
||||
|
listing platform, or any business that requires the handling of |
||||
|
multiple images per record, this module is built for you. Enjoy: |
||||
|
|
||||
|
- **E-commerce websites:** Easily manage product images with a |
||||
|
simple drag and drop interface. |
||||
|
- **Businesses with heavy media content:** Quickly update |
||||
|
portfolios, galleries, and listings. |
||||
|
- **Developers and Designers:** Seamlessly integrate modern |
||||
|
widgets across all modules—no extra dependency required! |
||||
|
|
||||
|
.. rubric:: Use Cases |
||||
|
:name: use-cases |
||||
|
|
||||
|
**Online Stores:** Showcase products with multiple images that can |
||||
|
be easily updated on-the-fly. |
||||
|
|
||||
|
**Real Estate Platforms:** Effortlessly manage property photos, |
||||
|
floor plans, and virtual tours. |
||||
|
|
||||
|
**Agencies & Portfolios:** Create visually appealing portfolios |
||||
|
where media speaks louder than words. |
||||
|
|
||||
|
.. rubric:: Benefits |
||||
|
:name: benefits |
||||
|
|
||||
|
With our module, say goodbye to tedious file uploads and hello to |
||||
|
a more engaging user experience. It’s simple, robust, and designed |
||||
|
to boost your productivity! |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: installation |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Installation |
||||
|
:name: installation |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
Follow these detailed steps to install the **eis_drag_drop** |
||||
|
module: |
||||
|
|
||||
|
#. **Download the Module:** Obtain the module as a ZIP file from |
||||
|
the official repository or your vendor. |
||||
|
#. **Extract the ZIP:** Unzip the downloaded file to reveal the |
||||
|
module folder. |
||||
|
#. **Copy to Addons Folder:** Move or copy the extracted module |
||||
|
folder into your Odoo ``addons`` directory. |
||||
|
#. **Restart Odoo Service:** |
||||
|
|
||||
|
- On systemd-based systems: ``sudo systemctl restart odoo`` |
||||
|
- On SysVinit-based systems: ``sudo service odoo restart`` |
||||
|
- Or use the appropriate command for your OS. |
||||
|
|
||||
|
#. **Update App List:** In your Odoo database, navigate to the |
||||
|
Apps menu and click *Update Apps List*. |
||||
|
#. **Install the Module:** Search for *Drag and Drop Widgets* (or |
||||
|
``eis_drag_drop``) and click install. |
||||
|
|
||||
|
.. container:: tips |
||||
|
|
||||
|
**Important:** Always test the module in a staging environment |
||||
|
before deploying it to your production server. Caution is |
||||
|
key—ensure you have backups and proper rollback plans in place. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: user-guides |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
User Guide |
||||
|
:name: user-guide |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. rubric:: Updating Existing Image Fields |
||||
|
:name: updating-existing-image-fields |
||||
|
|
||||
|
You can update any existing binary image field to use our drag and |
||||
|
drop widget without altering your custom modules. Simply update |
||||
|
your XML views by setting the widget attribute to: |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<field name="image_field" widget="d_and_d_image" options="{ |
||||
|
"image_size": "150x150", |
||||
|
"preview_image": "image_128", |
||||
|
"acceptedFileExtensions": "image/*", |
||||
|
"enableZoom": true, |
||||
|
"additionalStyles": "border:2px solid #007bff;" |
||||
|
}> |
||||
|
</field> |
||||
|
|
||||
|
**Options Explained:** |
||||
|
|
||||
|
- ``image_size``: Defines the display size of the widget. Example: |
||||
|
"150x150" sets the width to 150px and height to 150px. |
||||
|
- ``preview_image``: Specifies the field used for the image |
||||
|
preview. If not set, it defaults to the binary field name. |
||||
|
- ``acceptedFileExtensions``: Determines which image formats can |
||||
|
be uploaded. |
||||
|
- ``enableZoom``: If true, hovering over the image displays a zoom |
||||
|
popup. |
||||
|
- ``additionalStyles``: Custom CSS styles that are appended to the |
||||
|
widget's inline styles. |
||||
|
|
||||
|
.. rubric:: Detailed O2M Setup & Custom Integration |
||||
|
:name: detailed-o2m-setup-custom-integration |
||||
|
|
||||
|
For One2Many fields, our widget transforms the standard image |
||||
|
upload into a dynamic drag and drop area. **Note:** The related |
||||
|
model *must* have a Kanban view defined that includes at least the |
||||
|
``name`` and ``image`` (or ``image_1920``) fields. |
||||
|
|
||||
|
**Why a Kanban View?** The Kanban view enables the widget to |
||||
|
render image previews instead of just record IDs. Without it, you |
||||
|
may only see numerical identifiers. |
||||
|
|
||||
|
**Form View for Videos & Manual Entry:** If you wish to add videos |
||||
|
or manually add images in the traditional way, ensure that your |
||||
|
One2Many field is also accessible via a form view. |
||||
|
|
||||
|
.. rubric:: Example of One2Many Field Integration |
||||
|
:name: example-of-one2many-field-integration |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<page name="media" string="Extra Media"> |
||||
|
<field name="media_ids" widget="d_and_d_images" |
||||
|
options="{ |
||||
|
'childImageField': 'image_1920', |
||||
|
'extraData': { |
||||
|
'categ_id': 1, |
||||
|
'enable_zoom': true, |
||||
|
'previewImage': 'image_128', |
||||
|
'cssStyles': 'width:150px; height:200px; border-radius:15px;' |
||||
|
} |
||||
|
}"/> |
||||
|
</page> |
||||
|
|
||||
|
.. rubric:: Options Explained: |
||||
|
:name: options-explained |
||||
|
|
||||
|
- ``childImageField``: Specifies the field in the child model |
||||
|
where the binary image data is stored. This is mandatory for the |
||||
|
widget to function correctly. If not specified, image_1920 will |
||||
|
be used. |
||||
|
- ``extraData``: Allows you to pass additional data to the child |
||||
|
records. This can include: |
||||
|
|
||||
|
- ``categ_id``: An example field that sets a default category |
||||
|
ID. This feature allows you to add any additional field and |
||||
|
its value if your model has a required field and without it, |
||||
|
it cannot create new records, or a field that you need to fill |
||||
|
with static data. |
||||
|
- ``sale_ok``: This is another example field and you can pass |
||||
|
any bool value if your required field is bool type |
||||
|
- ``previewImage``: Specifies a smaller image field to use for |
||||
|
previews, which is important if you want to show a smaller |
||||
|
size image to save bandwidth. This value should be used if |
||||
|
your model is inherited from ``image.mixin`` and you want to |
||||
|
display a smaller preview image. |
||||
|
- ``cssStyles``: Custom CSS styles for the image display. |
||||
|
Example: |
||||
|
``"width:150px; height:200px; border-radius:15px; object-fit:cover;"``. |
||||
|
|
||||
|
- ``acceptedFileExtensions``: Determines which file types are |
||||
|
acceptable for upload. Default is ``"image/*"``. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: user-guides |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
User Guide |
||||
|
:name: user-guide-1 |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. rubric:: Updating Existing Image Fields |
||||
|
:name: updating-existing-image-fields-1 |
||||
|
|
||||
|
You can update any existing binary image field to use our drag and |
||||
|
drop widget without altering your custom modules. Simply update |
||||
|
your XML views by setting the widget attribute to: |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<field name="image_field" widget="d_and_d_image" options="{ |
||||
|
"image_size": "150x150", |
||||
|
"preview_image": "image_128", |
||||
|
"acceptedFileExtensions": "image/*", |
||||
|
"enableZoom": true, |
||||
|
"additionalStyles": "border:2px solid #007bff;" |
||||
|
}> |
||||
|
</field> |
||||
|
|
||||
|
**Options Explained:** |
||||
|
|
||||
|
- ``image_size``: Defines the display size of the widget. Example: |
||||
|
"150x150" sets the width to 150px and height to 150px. |
||||
|
- ``preview_image``: Specifies the field used for the image |
||||
|
preview. If not set, it defaults to the binary field name. |
||||
|
- ``acceptedFileExtensions``: Determines which image formats can |
||||
|
be uploaded. |
||||
|
- ``enableZoom``: If true, hovering over the image displays a zoom |
||||
|
popup. |
||||
|
- ``additionalStyles``: Custom CSS styles that are appended to the |
||||
|
widget's inline styles. |
||||
|
|
||||
|
.. rubric:: Detailed O2M Setup & Custom Integration |
||||
|
:name: detailed-o2m-setup-custom-integration-1 |
||||
|
|
||||
|
For One2Many fields, our widget transforms the standard image |
||||
|
upload into a dynamic drag and drop area. **Note:** The related |
||||
|
model *must* have a Kanban view defined that includes at least the |
||||
|
``name`` and ``image`` (or ``image_1920``) fields. |
||||
|
|
||||
|
**Why a Kanban View?** The Kanban view enables the widget to |
||||
|
render image previews instead of just record IDs. Without it, you |
||||
|
may only see numerical identifiers. |
||||
|
|
||||
|
**Form View for Videos & Manual Entry:** If you wish to add videos |
||||
|
or manually add images in the traditional way, ensure that your |
||||
|
One2Many field is also accessible via a form view. |
||||
|
|
||||
|
.. rubric:: Example of One2Many Field Integration |
||||
|
:name: example-of-one2many-field-integration-1 |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<page name="media" string="Extra Media"> |
||||
|
<field name="media_ids" widget="d_and_d_images" |
||||
|
options="{ |
||||
|
'childImageField': 'image_1920', |
||||
|
'extraData': { |
||||
|
'categ_id': 1, |
||||
|
'enable_zoom': true, |
||||
|
'previewImage': 'image_128', |
||||
|
'cssStyles': 'width:150px; height:200px; border-radius:15px;' |
||||
|
} |
||||
|
}"/> |
||||
|
</page> |
||||
|
|
||||
|
.. rubric:: Options Explained: |
||||
|
:name: options-explained-1 |
||||
|
|
||||
|
- ``childImageField``: Specifies the field in the child model |
||||
|
where the binary image data is stored. This is mandatory for the |
||||
|
widget to function correctly. If not specified, ``image_1920`` |
||||
|
will be used. |
||||
|
- ``extraData``: Allows you to pass additional data to the child |
||||
|
records. This can include: |
||||
|
|
||||
|
- ``categ_id``: An example field that sets a default category |
||||
|
ID. This feature allows you to add any additional field and |
||||
|
its value if your model has a required field and without it, |
||||
|
it cannot create new records, or a field that you need to fill |
||||
|
with static data. |
||||
|
- ``sale_ok``: This is another example field and you can pass |
||||
|
any bool value if your required field is bool type. |
||||
|
- ``previewImage``: Specifies a smaller image field to use for |
||||
|
previews, which is important if you want to show a smaller |
||||
|
size image to save bandwidth. This value should be used if |
||||
|
your model is inherited from ``image.mixin`` and you want to |
||||
|
display a smaller preview image. |
||||
|
- ``cssStyles``: Custom CSS styles for the image display. |
||||
|
Example: |
||||
|
``"width:150px; height:200px; border-radius:15px; object-fit:cover;"``. |
||||
|
|
||||
|
- ``acceptedFileExtensions``: Determines which file types are |
||||
|
acceptable for upload. Default is ``"image/*"``. |
||||
|
|
||||
|
.. container:: alert alert-danger |
||||
|
|
||||
|
.. rubric:: Important Notice! |
||||
|
:name: important-notice |
||||
|
:class: alert-heading |
||||
|
|
||||
|
Do not create the models described in the following example. |
||||
|
Odoo already includes these models. This example is provided |
||||
|
for educational purposes to help you understand the |
||||
|
implementation. |
||||
|
|
||||
|
.. rubric:: Complete Example (Taken from Odoo builtin Code): |
||||
|
:name: complete-example-taken-from-odoo-builtin-code |
||||
|
|
||||
|
.. rubric:: Adding Multiple Image Support to ``product.template`` |
||||
|
:name: adding-multiple-image-support-to-product.template |
||||
|
|
||||
|
Let's say we have a model ``product.template`` and we need to add |
||||
|
multiple image support to it. We will create a model |
||||
|
``product.image`` by inheriting from the ``image.mixin`` class. |
||||
|
This way, we automatically get fields like ``image_1920`` and |
||||
|
other related fields for our model ``product.image``. |
||||
|
|
||||
|
Here are the steps to achieve this: |
||||
|
|
||||
|
#. **Create the ``product.image`` Model:** |
||||
|
|
||||
|
Inherit from ``image.mixin`` to automatically get image-related |
||||
|
fields. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
class ProductImage(models.Model): |
||||
|
_name = 'product.image' |
||||
|
_inherit = 'image.mixin' |
||||
|
|
||||
|
product_tmpl_id = fields.Many2one('product.template', string='Product Template') |
||||
|
sequence = fields.Integer('Sequence') |
||||
|
video_url = fields.Char('Video URL') |
||||
|
|
||||
|
#. **Extend the ``product.template`` Model:** |
||||
|
|
||||
|
Add a One2Many field to link multiple images. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
class ProductTemplate(models.Model): |
||||
|
_inherit = 'product.template' |
||||
|
|
||||
|
product_template_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images') |
||||
|
|
||||
|
#. **Create a Kanban View for ``product.image``:** |
||||
|
|
||||
|
Add a Kanban view with the necessary fields. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<record id="view_product_image_kanban" model="ir.ui.view"> |
||||
|
<field name="name">product.image.kanban</field> |
||||
|
<field name="model">product.image</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<kanban> |
||||
|
<field name="id"/> |
||||
|
<field name="name"/> |
||||
|
<field name="image_1920" widget="image"/> |
||||
|
<field name="sequence"/> |
||||
|
</kanban> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
#. **Create a Form View for ``product.image``:** |
||||
|
|
||||
|
Add a form view with fields for video URL and image. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<record id="view_product_image_form" model="ir.ui.view"> |
||||
|
<field name="name">product.image.form</field> |
||||
|
<field name="model">product.image</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<field name="id"/> |
||||
|
<field name="name"/> |
||||
|
<field name="video_url"/> |
||||
|
<field name="image_1920" widget="image"/> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
#. **Update the ``product.template`` Form View:** |
||||
|
|
||||
|
Add the One2Many field with the drag-and-drop widget. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<record id="view_product_template_form" model="ir.ui.view"> |
||||
|
<field name="name">product.template.form</field> |
||||
|
<field name="model">product.template</field> |
||||
|
<field name="inherit_id" ref="product.product_template_only_form_view"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<sheet> |
||||
|
<group> |
||||
|
<field name="product_template_image_ids" widget="d_and_d_images" options="{ |
||||
|
'childImageField': 'image_1920', |
||||
|
'extraData': { |
||||
|
'cssStyles': 'width:150px;height:150px;' |
||||
|
} |
||||
|
}"/> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
.. container:: alert alert-warning |
||||
|
|
||||
|
**Important Considerations for Product Image Management (Odoo |
||||
|
17 and 18)** |
||||
|
This guide outlines the necessary steps for integrating |
||||
|
d_and_d_images widget for product template, specifically when |
||||
|
using the ``website_sale`` module, across Odoo 17 and 18. |
||||
|
Please carefully follow the instructions for your respective |
||||
|
Odoo version to avoid conflicts and ensure proper |
||||
|
functionality. |
||||
|
|
||||
|
**Odoo 17:** |
||||
|
|
||||
|
#. **Do not duplicate the ``product.image`` model or its |
||||
|
views.** This model is already provided by the |
||||
|
``website_sale`` module in Odoo. Creating a duplicate will |
||||
|
lead to conflicts. |
||||
|
#. **Inherit the ``product.template`` form view.**. You will |
||||
|
modify the form view ``product.product_template_form_view`` |
||||
|
to integrate d_and_d_images customizations seamlessly with |
||||
|
Odoo's core functionality. |
||||
|
#. **Customize the image field:** Replace the |
||||
|
``product_template_image_ids`` field in the inherited form |
||||
|
view with d_and_d_images widget as described above to |
||||
|
enhance the product image management functionality. |
||||
|
|
||||
|
**Odoo 18:** |
||||
|
|
||||
|
Odoo 18 has modified the ``product.image`` Kanban view by |
||||
|
removing certain fields. So, to make it compatible again with |
||||
|
our d_and_d_images widghet, here's how to adjust Odoo 18: |
||||
|
|
||||
|
#. **Inherit the ``product.image`` Kanban view.** |
||||
|
``website_sale.product_image_view_kanban``. Add the |
||||
|
necessary ``id`` and ``name`` fields back into the view to |
||||
|
maintain existing functionality. That's it. |
||||
|
#. **Enable modifications through the module's manifest:** |
||||
|
|
||||
|
- Declare dependency on ``website_sale`` in our |
||||
|
eis_drag_drop module's ``__manifest__.py`` file to ensure |
||||
|
proper loading order. You jsut need to uncomment the line |
||||
|
having website_sale with key depends and then comment the |
||||
|
existing depend key. |
||||
|
- Reference the XML file containing your Kanban view |
||||
|
modifications in the ``data`` key of the manifest to apply |
||||
|
these changes automatically when your module is installed |
||||
|
or updated. |
||||
|
|
||||
|
#. **Alternative approach:** Manually define the Kanban view |
||||
|
inheritance in a ``product.xml`` file if you prefer more |
||||
|
direct control over the modifications. |
||||
|
|
||||
|
Our module has pre-configured these enhancements for you. To |
||||
|
activate them, modify the ``__manifest__.py`` file to uncomment |
||||
|
the dependency on ``website_sale`` and the reference to |
||||
|
``/views/product.xml`` in the ``data`` key. This ensures all |
||||
|
functionalities are seamlessly integrated when you install or |
||||
|
update the module. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: screenshots |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Screenshots Explaination |
||||
|
:name: screenshots-explaination |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
01 |
||||
|
|
||||
|
.. rubric:: Drag and Drop Widget Overview |
||||
|
:name: drag-and-drop-widget-overview |
||||
|
|
||||
|
|Drag and Drop Widget Overview| |
||||
|
A clear view of the drag and drop widget areas for single |
||||
|
binary image and for one2many images., allowing users to |
||||
|
effortlessly update images. |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
02 |
||||
|
|
||||
|
.. rubric:: One2Many Drag And Drop Explained |
||||
|
:name: one2many-drag-and-drop-explained |
||||
|
|
||||
|
|One2many Field Enhancement| |
||||
|
In this screenshot, you can see a simple explanation of the |
||||
|
view. Each record's thumbnail, have a small cross on top right |
||||
|
corner to remove a record, view also display video thumbnail, |
||||
|
and buttons. |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
04 |
||||
|
|
||||
|
.. rubric:: Add Video Button |
||||
|
:name: add-video-button |
||||
|
|
||||
|
|image1| |
||||
|
This screenshot illustrates how the one2many field widget |
||||
|
``'Add Video'`` button is working. You can access odoo builtin |
||||
|
method being used in website_sale module of odoo to add extra |
||||
|
media including videos or images. |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
05 |
||||
|
|
||||
|
.. rubric:: Manual File Uploding Button Display |
||||
|
:name: manual-file-uploding-button-display |
||||
|
|
||||
|
|image2| |
||||
|
This screenshot illustrates how the one2many field widget |
||||
|
manual upload button is working. If you want to upload images |
||||
|
manually from file uploader, you can click on this button and |
||||
|
it will open finder / file explorer for you. It will only allow |
||||
|
images to upload with jpg, jpeg, png, svg etc |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
06 |
||||
|
|
||||
|
.. rubric:: Iamges Dragging over One2many Field Display |
||||
|
:name: iamges-dragging-over-one2many-field-display |
||||
|
|
||||
|
|image3| |
||||
|
This screenshot illustrates how the one2many field is being |
||||
|
used to drop 4 images into drag drop area. Notice the blue |
||||
|
border that is highlighted when we dragged the images over it. |
||||
|
As soon as we will drop the images, they will be uploaded |
||||
|
automatically |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: video |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Video Explaination |
||||
|
:name: video-explaination |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. container:: screenshot-full |
||||
|
|
||||
|
.. container:: screenshot-number |
||||
|
|
||||
|
01 |
||||
|
|
||||
|
.. rubric:: Drag and Drop Widget Overview |
||||
|
:name: drag-and-drop-widget-overview-1 |
||||
|
|
||||
|
.. container:: video-container |
||||
|
|
||||
|
.. container:: iframe |
||||
|
|
||||
|
.. container:: |
||||
|
:name: player |
||||
|
|
||||
|
.. container:: player-unavailable |
||||
|
|
||||
|
.. rubric:: ایک خرابی پیش آ گئی۔ |
||||
|
:name: ایک-خرابی-پیش-آ-گئی |
||||
|
:class: message |
||||
|
|
||||
|
.. container:: submessage |
||||
|
|
||||
|
`اس ویڈیو کو www.youtube.com پر دیکھنے کی کوشش |
||||
|
کریں <https://www.youtube.com/watch?v=JRPchiEhS9U>`__ |
||||
|
یا اگر آپ کے براؤزر میں JavaScript غیر فعال ہے تو |
||||
|
اسے فعال کریں۔ |
||||
|
|
||||
|
Watch the video to see how it works. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: version-info |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Module Version Information & Changelog |
||||
|
:name: module-version-information-changelog |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. container:: version-info |
||||
|
|
||||
|
- **v1.0.0** - Initial release with basic drag and drop |
||||
|
functionality. |
||||
|
- **v1.1.0** - Added support for one2many fields and improved |
||||
|
preview handling. |
||||
|
- **v1.2.0** - Enhanced UI, added zoom popup, and refined |
||||
|
installation instructions. |
||||
|
- **v1.3.0** - Bug fixes and performance improvements for large |
||||
|
image uploads. |
||||
|
- **v1.3.1** - New feature added to save dirty form to preserver new changes. |
||||
|
- 1: - add new key 'showConfirm' in extraData, it is bool value. it will show a confirmation dialog to ask user to save changes in form or not. |
||||
|
- 2: - If no option given, the default is true. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: services |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
90 Days Support |
||||
|
:name: days-support |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. container:: row text-center |
||||
|
|
||||
|
.. container:: col-md-4 mb-3 |
||||
|
|
||||
|
.. container:: service-tile |
||||
|
|
||||
|
.. rubric:: Website |
||||
|
:name: website |
||||
|
|
||||
|
`www.expertpk.com <https://www.expertpk.com>`__ |
||||
|
|
||||
|
.. container:: col-md-4 mb-3 |
||||
|
|
||||
|
.. container:: service-tile |
||||
|
|
||||
|
.. rubric:: Email |
||||
|
:name: email |
||||
|
|
||||
|
support@expertpk.com |
||||
|
|
||||
|
.. container:: col-md-4 mb-3 |
||||
|
|
||||
|
.. container:: service-tile |
||||
|
|
||||
|
.. rubric:: WhatsApp |
||||
|
:name: whatsapp |
||||
|
|
||||
|
+92 300 7888120 |
||||
|
|
||||
|
.. container:: section py-5 bg-light |
||||
|
:name: services |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: |
||||
|
Our Services |
||||
|
:name: our-services |
||||
|
:class: text-center mb-5 fw-bold |
||||
|
|
||||
|
.. container:: row g-4 |
||||
|
|
||||
|
.. container:: col-md-6 col-lg-3 |
||||
|
|
||||
|
.. container:: card h-100 text-center border-0 shadow-sm |
||||
|
|
||||
|
.. container:: card-body |
||||
|
|
||||
|
.. rubric:: Odoo Development |
||||
|
:name: odoo-development |
||||
|
:class: card-title |
||||
|
|
||||
|
Build custom Odoo modules and applications tailored to |
||||
|
your business needs. |
||||
|
|
||||
|
`Learn More <#contact>`__ |
||||
|
|
||||
|
.. container:: col-md-6 col-lg-3 |
||||
|
|
||||
|
.. container:: card h-100 text-center border-0 shadow-sm |
||||
|
|
||||
|
.. container:: card-body |
||||
|
|
||||
|
.. rubric:: Customization |
||||
|
:name: customization |
||||
|
:class: card-title |
||||
|
|
||||
|
Customize Odoo to fit your unique workflows and |
||||
|
business processes. |
||||
|
|
||||
|
`Learn More <#contact>`__ |
||||
|
|
||||
|
.. container:: col-md-6 col-lg-3 |
||||
|
|
||||
|
.. container:: card h-100 text-center border-0 shadow-sm |
||||
|
|
||||
|
.. container:: card-body |
||||
|
|
||||
|
.. rubric:: Upgradation |
||||
|
:name: upgradation |
||||
|
:class: card-title |
||||
|
|
||||
|
Upgrade your Odoo instance to the latest version with |
||||
|
zero downtime. |
||||
|
|
||||
|
`Learn More <#contact>`__ |
||||
|
|
||||
|
.. container:: col-md-6 col-lg-3 |
||||
|
|
||||
|
.. container:: card h-100 text-center border-0 shadow-sm |
||||
|
|
||||
|
.. container:: card-body |
||||
|
|
||||
|
.. rubric:: Consultancy |
||||
|
:name: consultancy |
||||
|
:class: card-title |
||||
|
|
||||
|
Get expert advice on Odoo implementation, |
||||
|
optimization, and best practices. |
||||
|
|
||||
|
`Learn More <#contact>`__ |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: other-modules |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: Explore Our Other Modules |
||||
|
:name: explore-our-other-modules |
||||
|
|
||||
|
.. container:: carousel slide module-slider |
||||
|
:name: modulesCarousel |
||||
|
|
||||
|
.. container:: carousel-inner |
||||
|
|
||||
|
.. container:: carousel-item active |
||||
|
|
||||
|
|Module 1| |
||||
|
|
||||
|
.. container:: carousel-caption d-none d-md-block |
||||
|
|
||||
|
.. rubric:: Module 1: Advanced Reporting |
||||
|
:name: module-1-advanced-reporting |
||||
|
|
||||
|
.. container:: carousel-item |
||||
|
|
||||
|
|Module 2| |
||||
|
|
||||
|
.. container:: carousel-caption d-none d-md-block |
||||
|
|
||||
|
.. rubric:: Module 2: CRM Enhancements |
||||
|
:name: module-2-crm-enhancements |
||||
|
|
||||
|
.. container:: carousel-item |
||||
|
|
||||
|
|Module 3| |
||||
|
|
||||
|
.. container:: carousel-caption d-none d-md-block |
||||
|
|
||||
|
.. rubric:: Module 3: Inventory Optimizer |
||||
|
:name: module-3-inventory-optimizer |
||||
|
|
||||
|
Previous |
||||
|
Next |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: best-practices |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: Best Practices & Troubleshooting |
||||
|
:name: best-practices-troubleshooting |
||||
|
|
||||
|
.. container:: tips |
||||
|
|
||||
|
.. rubric:: Best Practices |
||||
|
:name: best-practices |
||||
|
|
||||
|
- Always backup your database before applying new modules. |
||||
|
- Test the module in a staging environment prior to production |
||||
|
deployment. |
||||
|
- Ensure that the related one2many models have the required |
||||
|
Kanban view with mandatory fields. |
||||
|
- Regularly update your Odoo instance to keep up with security |
||||
|
and performance improvements. |
||||
|
|
||||
|
.. container:: troubleshooting |
||||
|
|
||||
|
.. rubric:: Troubleshooting Steps |
||||
|
:name: troubleshooting-steps |
||||
|
|
||||
|
- If image previews are not displaying, verify that your Kanban |
||||
|
view contains the ``name`` and ``image``/``image_1920`` |
||||
|
fields. |
||||
|
- Check the browser console for JavaScript errors related to |
||||
|
the widget. |
||||
|
- Ensure file permissions are correctly set in your addons |
||||
|
folder. |
||||
|
- Review the server logs for any errors during file upload or |
||||
|
service restart. |
||||
|
|
||||
|
.. container:: section section |
||||
|
:name: code-snippets |
||||
|
|
||||
|
.. container:: |
||||
|
|
||||
|
.. rubric:: Code Snippets |
||||
|
:name: code-snippets |
||||
|
|
||||
|
Below are examples of typical XML view definitions where the new |
||||
|
drag and drop fields are highlighted: |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
.. rubric:: Single Image Widget |
||||
|
:name: single-image-widget |
||||
|
|
||||
|
This snippet shows how to integrate a single image drag and |
||||
|
drop field into an Odoo form view: |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<form string="Product Form"> |
||||
|
<sheet> |
||||
|
<group> |
||||
|
<field name="name" /> |
||||
|
<field name="description" /> |
||||
|
<!-- Our new drag and drop image field is highlighted below --> |
||||
|
<field name="image_1920" widget="d_and_d_image" class="oe_avator" options="{ |
||||
|
"image_size": "200x200", |
||||
|
"preview_image": "image_128", |
||||
|
"acceptedFileExtensions": "image/*", |
||||
|
"enableZoom": true |
||||
|
}" /> |
||||
|
<field name="price" /> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
|
||||
|
**Explanation:** This code defines a form view for a product. |
||||
|
The ``product_image`` field uses the ``d_and_d_image`` widget, |
||||
|
which allows users to drag and drop images. The ``options`` |
||||
|
attribute specifies settings like image size, preview image, |
||||
|
accepted file extensions, and zoom functionality. |
||||
|
|
||||
|
.. container:: code-snippet |
||||
|
|
||||
|
.. rubric:: One2Many Image Widget |
||||
|
:name: one2many-image-widget |
||||
|
|
||||
|
This snippet demonstrates how to integrate a One2Many image |
||||
|
drag and drop field into an Odoo form view: |
||||
|
|
||||
|
:: |
||||
|
|
||||
|
<form string="Product Form"> |
||||
|
<sheet> |
||||
|
<group> |
||||
|
<field name="name" /> |
||||
|
<field name="description" /> |
||||
|
<!-- One2Many drag and drop image field --> |
||||
|
<field name="product_images" widget="d_and_d_images" options="{ |
||||
|
"childImageField": "image_1920", |
||||
|
"extraData": { |
||||
|
"cssStyles": "width:150px;height:150px;" |
||||
|
} |
||||
|
}" /> |
||||
|
<field name="price" /> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
|
||||
|
**Explanation:** This code defines a form view for a product |
||||
|
with a One2Many field ``product_images``. The |
||||
|
``d_and_d_images`` widget allows users to drag and drop |
||||
|
multiple images. The ``options`` attribute specifies the child |
||||
|
image field and additional CSS styles for the image display. |
||||
|
|
||||
|
© 2025 Expert IT Solutions. All rights reserved. |
||||
|
|
||||
|
.. |Logo| image:: ../static/description/img/logo.png |
||||
|
.. |Hero Vector| image:: ../static/description/img/hero.svg |
||||
|
.. |Drag and Drop Widget Overview| image:: ../static/description/img/image1.png |
||||
|
:class: screenshot-img |
||||
|
.. |One2many Field Enhancement| image:: ../static/description/img/image3.png |
||||
|
:class: screenshot-img |
||||
|
.. |image1| image:: ../static/description/img/image4.png |
||||
|
:class: screenshot-img |
||||
|
.. |image2| image:: ../static/description/img/image5.png |
||||
|
:class: screenshot-img |
||||
|
.. |image3| image:: ../static/description/img/image6.png |
||||
|
:class: screenshot-img |
||||
|
.. |Module 1| image:: ../static/description/module1.png |
||||
|
:class: d-block w-100 |
||||
|
.. |Module 2| image:: ../static/description/module2.png |
||||
|
:class: d-block w-100 |
||||
|
.. |Module 3| image:: ../static/description/module3.png |
||||
|
:class: d-block w-100 |
@ -0,0 +1 @@ |
|||||
|
from . import models |
@ -0,0 +1,87 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################################# |
||||
|
# Author : Expert IT Solutions (<www.expertpk.com>) |
||||
|
# Copyright(c): 2012-Present Expert IT Solutions |
||||
|
# All Rights Reserved. |
||||
|
# |
||||
|
# This program is copyright property of the author mentioned above. |
||||
|
# You can`t redistribute it and/or modify it. |
||||
|
# |
||||
|
################################################################################# |
||||
|
from odoo import models, fields, api |
||||
|
from lxml import etree |
||||
|
import logging |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class Base(models.AbstractModel): |
||||
|
_inherit = 'base' |
||||
|
|
||||
|
|
||||
|
@api.model |
||||
|
def get_view(self, view_id=None, view_type='form', **options): |
||||
|
res = super().get_view(view_id, view_type, **options) |
||||
|
# Parse the XML architecture from the result |
||||
|
doc = etree.XML(res['arch']) |
||||
|
if view_type == 'form': |
||||
|
for node in doc.xpath("//field[@widget='d_and_d_images']"): |
||||
|
_logger.info("Found node: %s", node.get('name')) |
||||
|
field_name = node.get('name') |
||||
|
|
||||
|
# Create a new field node with kanban mode and style display none |
||||
|
new_node = etree.Element('field', { |
||||
|
'name': field_name, |
||||
|
'mode': 'kanban', |
||||
|
'nolabel': '1', |
||||
|
'style': 'display:none; max-width: 1px; max-height: 1px;', |
||||
|
'context': "{'default_name': name}", |
||||
|
}) |
||||
|
parent = node.getparent() |
||||
|
index = parent.index(node) |
||||
|
parent.insert(index - 1, new_node) |
||||
|
|
||||
|
res['arch'] = etree.tostring(doc, pretty_print=True, encoding='unicode') |
||||
|
return res |
||||
|
return res |
||||
|
|
||||
|
|
||||
|
class IrAttachment(models.Model): |
||||
|
_inherit = "ir.attachment" |
||||
|
|
||||
|
@api.model |
||||
|
def action_save_drag_and_drop_images(self, resModel, resId, resField, childField, fileDatas, extraData=None): |
||||
|
parent = self.env[resModel].browse(int(resId)) |
||||
|
if not parent.exists(): |
||||
|
_logger.error(f"Parent record not found: Model={resModel}, ID={resId}") |
||||
|
return False |
||||
|
|
||||
|
records_data = [] |
||||
|
for fileData in fileDatas: |
||||
|
if 'filename' in fileData and 'base64' in fileData: |
||||
|
record_vals = { |
||||
|
'name': fileData['filename'], |
||||
|
childField: fileData['base64'], |
||||
|
} |
||||
|
if extraData and isinstance(extraData, dict): |
||||
|
filtered_data = {k: v for k, v in extraData.items() if k not in ['cssStyles', 'previewImage', 'showConfirm']} |
||||
|
record_vals.update(filtered_data) # Merge any extra required fields |
||||
|
records_data.append(record_vals) |
||||
|
else: |
||||
|
_logger.warning("Missing 'filename' or 'base64' in file data") |
||||
|
|
||||
|
if records_data: |
||||
|
try: |
||||
|
parent.write({ |
||||
|
resField: [(0, 0, vals) for vals in records_data] |
||||
|
}) |
||||
|
_logger.info(f"Images successfully added to {resModel} ID {resId} in field {resField}") |
||||
|
return True |
||||
|
except Exception as e: |
||||
|
_logger.error(f"Error updating parent record: {e}") |
||||
|
return False |
||||
|
else: |
||||
|
_logger.info("No valid image data provided") |
||||
|
return False |
||||
|
|
||||
|
|
After Width: | Height: | Size: 12 MiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 229 KiB |
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 6.2 KiB |
@ -0,0 +1,49 @@ |
|||||
|
.o_dnd_images_widget { |
||||
|
width: 100%; |
||||
|
.o_dnd_drop_zone { |
||||
|
width: 100%; |
||||
|
min-height: 150px; |
||||
|
border: 2px dashed #ccc; |
||||
|
border-radius: 4px; |
||||
|
text-align: center; |
||||
|
position: relative; |
||||
|
transition: transform 0.2s ease-in-out; |
||||
|
.o_drop_area_text { |
||||
|
margin-top: 1em; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
.o_dnd_drop_zone.o_dnd_dragging { |
||||
|
border: 3px dashed #007bff; |
||||
|
transform: scale(1.02); |
||||
|
background-color: #f8f9fa; |
||||
|
} |
||||
|
.o_dnd_uploading { |
||||
|
color: #666; |
||||
|
} |
||||
|
.o_dnd_kanban_container { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
.o_dnd_kanban_card { |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 4px; |
||||
|
width: 120px; |
||||
|
overflow: hidden; |
||||
|
.o_card_body { |
||||
|
position: relative; |
||||
|
.o_dnd_kanban_image { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
display: block; |
||||
|
} |
||||
|
.o_dnd_kanban_info { |
||||
|
padding: 4px; |
||||
|
background: #f2f2f2; |
||||
|
font-size: 0.8rem; |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,249 @@ |
|||||
|
/* Enforce 100% width for the drag-and-drop zone */ |
||||
|
.o_dnd_images_widget { |
||||
|
width: 100%; |
||||
|
min-width: 100vh; |
||||
|
} |
||||
|
.o_dnd_drop_zone { |
||||
|
width: 100%; /* Force the container to expand to 100% width */ |
||||
|
min-height: 150px; /* Minimum height */ |
||||
|
border: 2px dashed #ccc; /* Dashed border */ |
||||
|
border-radius: 4px; /* Rounded corners */ |
||||
|
text-align: center; /* Center any child content */ |
||||
|
position: relative; /* Required for overlays/effects */ |
||||
|
transition: all 0.2s ease-in-out; /* Smooth interactions */ |
||||
|
background-color: #f8f8f8; /* Set a light background color so it's clear this area is the drop zone */ |
||||
|
box-sizing: border-box; /* Includes padding/border in layout */ |
||||
|
} |
||||
|
|
||||
|
/* Ensure the drop zone responds to drag events fully, not only the border */ |
||||
|
.o_dnd_drop_zone:hover, |
||||
|
.o_dnd_drop_zone.o_dnd_dragging { |
||||
|
background-color: #eef7ff; /* Highlight background during dragging */ |
||||
|
border-color: #007bff; /* Highlight border color */ |
||||
|
transform: scale(1.02); /* Slight zoom effect */ |
||||
|
} |
||||
|
|
||||
|
/* Add padding inside the drag-and-drop container */ |
||||
|
.o_dnd_drop_zone .o_dnd_kanban_container { |
||||
|
padding: 10px; /* Add spacing around the inner content */ |
||||
|
display: flex; /* Flex container for items inside drop zone */ |
||||
|
flex-wrap: wrap; /* Allow items to wrap to new rows */ |
||||
|
gap: 15px; /* Consistent spacing between tiles */ |
||||
|
justify-content: flex-start; /* Items start aligned to the left */ |
||||
|
align-items: flex-start; /* Top-aligned items */ |
||||
|
} |
||||
|
|
||||
|
/* Ensure the drop zone always maintains its design, even when empty */ |
||||
|
.o_dnd_drop_zone_empty { |
||||
|
display: flex; /* Center the "empty" placeholder */ |
||||
|
justify-content: center; /* Center horizontally */ |
||||
|
align-items: center; /* Center vertically */ |
||||
|
width: 100%; |
||||
|
height: 100%; /* Take full available drop zone space */ |
||||
|
color: #ccc; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
/* Flex-based item cards/tile styles */ |
||||
|
.o_dnd_kanban_card { |
||||
|
width: 120px; |
||||
|
height: 120px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
background-color: #fff; /* White background for cards */ |
||||
|
border: 1px solid #ddd; /* Subtle card border */ |
||||
|
border-radius: 4px; /* Rounded corners */ |
||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */ |
||||
|
position: relative; /* Required for close button positioning */ |
||||
|
} |
||||
|
|
||||
|
/* Close button inside cards */ |
||||
|
.o_dnd_kanban_card .close-button { |
||||
|
position: absolute; |
||||
|
top: -5px; |
||||
|
right: -5px; |
||||
|
background-color: red; |
||||
|
border: none; |
||||
|
color: white; |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
border-radius: 50%; |
||||
|
cursor: pointer; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
/* Manual upload tile */ |
||||
|
.o_image_tile_upload { |
||||
|
background-color: #f8f9fa; /* Light gray background */ |
||||
|
border: 2px dashed #ccc; /* Dashed border for upload tile */ |
||||
|
width: 120px; /* Same size as other tiles */ |
||||
|
height: 120px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
transition: all 0.15s ease-in-out; /* Hover effect */ |
||||
|
} |
||||
|
|
||||
|
.o_image_tile_upload:hover { |
||||
|
border-color: #007bff; /* Highlight border on hover */ |
||||
|
background-color: #eef7ff; /* Subtle highlight background */ |
||||
|
} |
||||
|
|
||||
|
/* Ensure the upload indicator displays properly */ |
||||
|
.o_dnd_uploading { |
||||
|
color: #666; |
||||
|
text-align: center; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
/* single image styles below */ |
||||
|
.dnd_widget_container { |
||||
|
position: relative; |
||||
|
margin: 10px; |
||||
|
} |
||||
|
|
||||
|
.dnd_drop_zone { |
||||
|
position: relative; |
||||
|
border: 2px dashed #ccc; |
||||
|
border-radius: 8px; |
||||
|
padding: 10px; |
||||
|
text-align: center; |
||||
|
transition: border-color 0.3s; |
||||
|
} |
||||
|
|
||||
|
.dnd_active_drag { |
||||
|
border-color: #007bff; |
||||
|
} |
||||
|
|
||||
|
/* Controls hidden initially */ |
||||
|
.dnd_image_controls { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-start; |
||||
|
gap: 10px; |
||||
|
padding: 10px; |
||||
|
pointer-events: none; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.dnd_drop_zone:hover .dnd_image_controls { |
||||
|
pointer-events: auto; |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
/* Edit Button (Top Left) */ |
||||
|
.dnd_edit_button { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
left: 10px; |
||||
|
} |
||||
|
|
||||
|
/* Upload Button (Top Right) */ |
||||
|
.dnd_upload_button { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
right: 10px; |
||||
|
} |
||||
|
|
||||
|
/* Remove Button (Bottom Left) */ |
||||
|
.dnd_remove_button { |
||||
|
position: absolute; |
||||
|
bottom: 10px; |
||||
|
left: 10px; |
||||
|
} |
||||
|
|
||||
|
.dnd_button { |
||||
|
background-color: white; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 50%; |
||||
|
padding: 5px; |
||||
|
cursor: pointer; |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
.dnd_button:hover { |
||||
|
background-color: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
/* Zoom popup remains unaffected */ |
||||
|
.dnd_zoom_popup { |
||||
|
position: absolute; |
||||
|
z-index: 9999; |
||||
|
transition: all 0.3s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
.dnd_image_preview { |
||||
|
display: block; |
||||
|
object-fit: cover; |
||||
|
border-radius: 5px; |
||||
|
transition: transform 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.dnd_image_placeholder { |
||||
|
color: #aaa; |
||||
|
font-size: 14px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.dnd_uploading_spinner { |
||||
|
margin-top: 10px; |
||||
|
text-align: center; |
||||
|
color: #007bff; |
||||
|
} |
||||
|
|
||||
|
/* Style for the zoomed image */ |
||||
|
.dnd_zoom_popup { |
||||
|
display: flex; /* Use flexbox for centering the image */ |
||||
|
justify-content: center; |
||||
|
align-items: flex-start; /* Ensure the image aligns at the top inside the popup */ |
||||
|
position: absolute; /* Position the popup relative to the original element */ |
||||
|
/*top: 0; !* Ensure it aligns properly from the top *!*/ |
||||
|
/*margin-top: 40px;*/ |
||||
|
left: -110%; /* Place the popup to the left of the original block */ |
||||
|
z-index: 9999; /* Ensure popup appears above other elements */ |
||||
|
background-color: #fff; /* Add a background for better contrast */ |
||||
|
width: auto; /* Fixed width of 720p resolution */ |
||||
|
height: auto; /* Fixed height of 720p resolution */ |
||||
|
padding: 10px; |
||||
|
border-radius: 10px; |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Add shadow for better visibility */ |
||||
|
border: 2px solid #007bff; /* Blue border for aesthetic */ |
||||
|
max-width: none; |
||||
|
max-height: none; |
||||
|
|
||||
|
/* Add scrollable behavior for content that overflows the fixed size */ |
||||
|
overflow: auto; /* Enable scrolling for overflow content */ |
||||
|
visibility: hidden; /* Hidden by default */ |
||||
|
opacity: 0; /* Initial opacity set to 0 */ |
||||
|
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
/* When zoom popup is visible */ |
||||
|
.dnd_zoom_popup.show { |
||||
|
visibility: visible; /* Make visible */ |
||||
|
opacity: 1; /* Add fade-in effect */ |
||||
|
} |
||||
|
|
||||
|
/* Zoomed image styling */ |
||||
|
.dnd_zoomed_image { |
||||
|
display: block; |
||||
|
/* Ensure the image expands or scrolls within the popup */ |
||||
|
max-width: 100%; /* Resize image width to fit popup */ |
||||
|
max-height: 100%; /* Resize image height to fit popup */ |
||||
|
width: auto; |
||||
|
height: auto; |
||||
|
object-fit: contain; /* Maintain the aspect ratio of the image */ |
||||
|
border-radius: 10px; /* Add border radius for aesthetics */ |
||||
|
} |
||||
|
/* single image styles above */ |
@ -0,0 +1,233 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import {registry} from "@web/core/registry"; |
||||
|
import {_t} from "@web/core/l10n/translation"; |
||||
|
import {FileUploader} from "@web/views/fields/file_handler"; |
||||
|
import {standardFieldProps} from "@web/views/fields/standard_field_props"; |
||||
|
import {url} from "@web/core/utils/urls"; |
||||
|
import {isBinarySize} from "@web/core/utils/binary"; |
||||
|
import {useService} from "@web/core/utils/hooks"; // optional if you want notifications
|
||||
|
import {Component, useState, useRef} from "@odoo/owl"; |
||||
|
import {onWillUpdateProps} from "@odoo/owl"; |
||||
|
|
||||
|
/** |
||||
|
* Enhanced single-image drag-and-drop widget with custom image size and preview features. |
||||
|
*/ |
||||
|
|
||||
|
const placeholder = "/web/static/img/placeholder.png"; |
||||
|
export class DndImageWidget extends Component { |
||||
|
static template = "eis_drag_drop.DndImageWidgetTemplate"; |
||||
|
static components = {FileUploader}; |
||||
|
static props = { |
||||
|
...standardFieldProps, |
||||
|
acceptedFileExtensions: {type: String, optional: true}, |
||||
|
image_size: {type: String, optional: true}, // Example: "100x150"
|
||||
|
preview_image: {type: String, optional: true}, // Example: "image_128"
|
||||
|
enableZoom: {type: Boolean, optional: true}, // Optional zoom functionality
|
||||
|
additionalStyles: {type: String, optional: true}, // Append custom styles
|
||||
|
}; |
||||
|
static defaultProps = { |
||||
|
acceptedFileExtensions: "image/*", |
||||
|
image_size: "100x150", |
||||
|
}; |
||||
|
|
||||
|
setup() { |
||||
|
this.notification = useService("notification"); |
||||
|
this.isUploading = useState({value: false}); |
||||
|
this.isDraggingOver = useState({value: false}); |
||||
|
this.dropZoneRef = useRef("dropZoneRef"); |
||||
|
this.zoomTimer = null; // Timer for delayed zoom
|
||||
|
this.showZoomPopup = useState({ value: false }); // Popup state
|
||||
|
|
||||
|
// Extract width/height from image_size
|
||||
|
const [width, height] = (this.props.image_size || "").split("x").map((value) => parseInt(value)); |
||||
|
this.imageWidth = isNaN(width) ? null : width; |
||||
|
this.imageHeight = isNaN(height) ? null : height; |
||||
|
|
||||
|
this.cacheKey = this.props.record.data.write_date; |
||||
|
onWillUpdateProps((nextProps) => { |
||||
|
const {record} = this.props; |
||||
|
const {record: nextRecord} = nextProps; |
||||
|
if (record.resId !== nextRecord.resId || nextRecord.mode === "readonly") { |
||||
|
this.cacheKey = nextRecord.data.write_date; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
// Event Methods
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
// Handles hover on image (shows zoom popup after 1 second)
|
||||
|
onMouseEnter() { |
||||
|
if (this.props.enableZoom) { |
||||
|
this.zoomTimer = setTimeout(() => { |
||||
|
this.showZoomPopup.value = true; // Display popup
|
||||
|
}, 1000); // Trigger after 1-second hover
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Handles mouse leave (hides zoom popup)
|
||||
|
onMouseLeave() { |
||||
|
if (this.zoomTimer) { |
||||
|
clearTimeout(this.zoomTimer); // Cancel timeout if mouse leaves
|
||||
|
this.zoomTimer = null; |
||||
|
} |
||||
|
this.showZoomPopup.value = false; // Hide popup
|
||||
|
} |
||||
|
|
||||
|
// Prevents the popup from hiding when interacting within it
|
||||
|
onMouseEnterPopup() { |
||||
|
this.showZoomPopup.value = true; // Ensure visibility on hover within the popup
|
||||
|
} |
||||
|
|
||||
|
onMouseLeavePopup() { |
||||
|
this.showZoomPopup.value = false; // Hide popup when mouse leaves
|
||||
|
} |
||||
|
|
||||
|
onDownloadOriginalImage() { |
||||
|
const anchor = document.createElement("a"); |
||||
|
anchor.href = this.originalImageUrl; |
||||
|
anchor.download = "original_image"; // Default filename
|
||||
|
anchor.click(); |
||||
|
console.log("[DndImageWidget] Download initiated:", this.originalImageUrl); |
||||
|
} |
||||
|
|
||||
|
// Returns the URL for the original image (full resolution)
|
||||
|
get originalImageUrl() { |
||||
|
return url("/web/image", { |
||||
|
model: this.props.record.resModel, |
||||
|
id: this.props.record.resId, |
||||
|
field: this.props.name, // Always load original image
|
||||
|
unique: this.cacheKey || "", |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
get imageUrl() { |
||||
|
const bin = this.props.record.data[this.props.name]; |
||||
|
console.log('bin: ', bin); |
||||
|
if (!bin) return placeholder; |
||||
|
if (isBinarySize(bin)) { |
||||
|
let recUrl = url("/web/image", { |
||||
|
model: this.props.record.resModel, |
||||
|
id: this.props.record.resId, |
||||
|
field: this.props.preview_image || this.props.name, |
||||
|
unique: this.cacheKey || "", |
||||
|
}); |
||||
|
console.log('recUrl: ', recUrl); |
||||
|
return recUrl; |
||||
|
} |
||||
|
return `data:image/png;base64,${bin}`; |
||||
|
} |
||||
|
|
||||
|
get imageStyle() { |
||||
|
let style = this.isDraggingOver.value ? "border: 2px dashed #007bff;" : ""; // Dashed border on drag
|
||||
|
if (this.imageWidth) { |
||||
|
style += `width: ${this.imageWidth}px;`; |
||||
|
} |
||||
|
if (this.imageHeight) { |
||||
|
style += `height: ${this.imageHeight}px;`; |
||||
|
} else { |
||||
|
style += "max-width: 100%; max-height: 100%;"; |
||||
|
} |
||||
|
if (this.props.additionalStyles) { |
||||
|
style += this.props.additionalStyles; // Append user-defined styles
|
||||
|
} |
||||
|
style += 'object-fit: cover;'; |
||||
|
return style; |
||||
|
} |
||||
|
// Refresh the image URL when a new image is uploaded or replaced
|
||||
|
refreshImage() { |
||||
|
this.cacheKey = Date.now(); // Generate a unique cache key to force image reloading
|
||||
|
this.render(); // Trigger re-render to update the image
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
// Event Handlers
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
onDrop(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDraggingOver.value = false; |
||||
|
const file = ev.dataTransfer.files[0]; |
||||
|
if (!file) return; |
||||
|
this._readFileAsDataUrl(file); |
||||
|
} |
||||
|
|
||||
|
onDragOver(ev) { |
||||
|
ev.preventDefault(); |
||||
|
} |
||||
|
|
||||
|
onDragEnter(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDraggingOver.value = true; |
||||
|
} |
||||
|
|
||||
|
onDragLeave(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDraggingOver.value = false; |
||||
|
} |
||||
|
|
||||
|
async _readFileAsDataUrl(file) { |
||||
|
this.isUploading.value = true; |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const dataUrl = e.target.result; |
||||
|
this._updateRecordWithBase64(dataUrl); |
||||
|
this.refreshImage(); // Call refreshImage to update the displayed image
|
||||
|
}; |
||||
|
reader.readAsDataURL(file); |
||||
|
} |
||||
|
|
||||
|
_updateRecordWithBase64(dataUrl) { |
||||
|
const base64Part = dataUrl.split(",")[1] || ""; |
||||
|
this.props.record.update({[this.props.name]: base64Part}); |
||||
|
this.isUploading.value = false; |
||||
|
} |
||||
|
|
||||
|
onFileRemove() { |
||||
|
this.props.record.update({ [this.props.name]: false }); |
||||
|
this.refreshImage(); // Refresh the image if it is removed
|
||||
|
} |
||||
|
|
||||
|
onLoadFailed() { |
||||
|
this.notification?.add(_t("Could not display the selected image"), { |
||||
|
type: "warning", |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onFileUploaded(info) { |
||||
|
if (info.data) { |
||||
|
const mimeSuffix = info.type.split("/")[1] || "png"; |
||||
|
const dataUrl = `data:image/${mimeSuffix};base64,${info.data}`; |
||||
|
this._updateRecordWithBase64(dataUrl); |
||||
|
|
||||
|
// Refresh the zoomed image after file upload
|
||||
|
this.refreshImage(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onClickImage() { |
||||
|
// Open image in a new tab
|
||||
|
window.open(this.originalImageUrl, "_blank"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Debugging the extractProps logic
|
||||
|
const dndImageField = { |
||||
|
component: DndImageWidget, |
||||
|
displayName: _t("D&D Single Image"), |
||||
|
supportedTypes: ["binary"], |
||||
|
extractProps: ({attrs, options}) => { |
||||
|
console.log("[DndImageWidget] extractProps invoked - attrs:", attrs, "options:", options); |
||||
|
|
||||
|
return { |
||||
|
image_size: options.image_size || "100x120", // Fallback to "100x150"
|
||||
|
preview_image: options.preview_image || attrs.name, // Fallback to field name
|
||||
|
acceptedFileExtensions: options.acceptedFileExtensions || "image/*", // Default to all image types
|
||||
|
enableZoom: options.enableZoom === "true" || false, // Ensure boolean value
|
||||
|
}; |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// Register the widget
|
||||
|
registry.category("fields").add("d_and_d_image", dndImageField); |
@ -0,0 +1,254 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
import { registry } from "@web/core/registry"; |
||||
|
import { _t } from "@web/core/l10n/translation"; |
||||
|
import { standardFieldProps } from "@web/views/fields/standard_field_props"; |
||||
|
import { FileUploader } from "@web/views/fields/file_handler"; |
||||
|
import { Component, useState, useRef } from "@odoo/owl"; |
||||
|
import { useService } from "@web/core/utils/hooks"; |
||||
|
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; |
||||
|
|
||||
|
export class DndImagesWidget extends Component { |
||||
|
static template = "eis_drag_drop.DndImagesWidgetTemplate"; |
||||
|
static components = { FileUploader }; |
||||
|
static props = { |
||||
|
...standardFieldProps, |
||||
|
childImageField: { type: String, optional: true }, |
||||
|
extraData: { type: Object, optional: true }, |
||||
|
}; |
||||
|
|
||||
|
setup() { |
||||
|
super.setup(); |
||||
|
this.orm = useService("orm"); |
||||
|
this.action = useService("action"); |
||||
|
this.notification = useService("notification"); |
||||
|
this.isUploading = useState({ value: false }); |
||||
|
this.isDragging = useState({ value: false }); |
||||
|
this.dropZoneRef = useRef("dropZoneRef"); |
||||
|
// If one2many not loaded, load it explicitly
|
||||
|
if (!this.props.record.data[this.props.name]) { |
||||
|
this.props.record.ensureOne2manyRecords(this.props.name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onDragOver(ev) { |
||||
|
ev.preventDefault(); |
||||
|
} |
||||
|
onDragEnter(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDragging.value = true; |
||||
|
} |
||||
|
onDragLeave(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDragging.value = false; |
||||
|
} |
||||
|
async onDrop(ev) { |
||||
|
ev.preventDefault(); |
||||
|
this.isDragging.value = false; |
||||
|
const files = ev.dataTransfer.files || []; |
||||
|
if (!files.length) { |
||||
|
return; |
||||
|
} |
||||
|
await this._handleMultipleFiles(files); |
||||
|
} |
||||
|
|
||||
|
async _handleMultipleFiles(files) { |
||||
|
this.isUploading.value = true; |
||||
|
try { |
||||
|
const extraData = this.props.extraData || {}; |
||||
|
// If parent record is dirty, confirm or just save
|
||||
|
if (this.props.record.isDirty) { |
||||
|
if (extraData.showConfirm) { |
||||
|
const confirmed = await new Promise(resolve => { |
||||
|
this.env.services.dialog.add(ConfirmationDialog, { |
||||
|
body: _t("The record has unsaved changes. Do you want to save them?"), |
||||
|
confirm: () => resolve(true), |
||||
|
cancel: () => resolve(false), |
||||
|
}); |
||||
|
}); |
||||
|
if (!confirmed) { |
||||
|
this.isUploading.value = false; |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
await this.props.record.save({ reload: true }); |
||||
|
} |
||||
|
|
||||
|
// Build fileDatas array
|
||||
|
const fileDatas = []; |
||||
|
for (let i = 0; i < files.length; i++) { |
||||
|
const file = files[i]; |
||||
|
if (!file.type.startsWith("image/")) { |
||||
|
continue; |
||||
|
} |
||||
|
const dataUrl = await this._readFile(file); |
||||
|
const base64Part = dataUrl.split(",")[1] || ""; |
||||
|
fileDatas.push({ base64: base64Part, filename: file.name }); |
||||
|
} |
||||
|
|
||||
|
if (fileDatas.length) { |
||||
|
const resModel = this.props.record.resModel; |
||||
|
const resId = this.props.record.resId; |
||||
|
const resField = this.props.name; |
||||
|
const childField = this.props.childImageField || "image_1920"; |
||||
|
|
||||
|
await this.orm.call("ir.attachment", "action_save_drag_and_drop_images", [ |
||||
|
resModel, |
||||
|
resId, |
||||
|
resField, |
||||
|
childField, |
||||
|
fileDatas, |
||||
|
extraData, |
||||
|
]); |
||||
|
await this.props.record.load(); |
||||
|
await this.props.record.model.notify(); |
||||
|
} else { |
||||
|
this.notification.add(_t("No valid image files found."), { type: "warning" }); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("Error uploading images:", error); |
||||
|
this.notification.add( |
||||
|
_t("Error uploading images. Check logs or required fields."), |
||||
|
{ type: "danger" } |
||||
|
); |
||||
|
} finally { |
||||
|
this.isUploading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_readFile(file) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = e => resolve(e.target.result); |
||||
|
reader.onerror = reject; |
||||
|
reader.readAsDataURL(file); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// When FileUploader (manual upload) fires:
|
||||
|
onFileUploaded = async (ev) => { |
||||
|
// If parent record is dirty, confirm or just save
|
||||
|
const extraData = this.props.extraData || {}; |
||||
|
if (this.props.record.isDirty) { |
||||
|
if (extraData.showConfirm) { |
||||
|
const confirmed = await new Promise(resolve => { |
||||
|
this.env.services.dialog.add(ConfirmationDialog, { |
||||
|
body: _t("The record has unsaved changes. Do you want to save them?"), |
||||
|
confirm: () => resolve(true), |
||||
|
cancel: () => resolve(false), |
||||
|
}); |
||||
|
}); |
||||
|
if (!confirmed) { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
await this.props.record.save({ reload: true }); |
||||
|
} |
||||
|
|
||||
|
if (!ev.data) { |
||||
|
this.notification.add(_t("No files uploaded or invalid file."), { type: "warning" }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.isUploading.value = true; |
||||
|
try { |
||||
|
const fileDatas = []; |
||||
|
// Normalize ev.data & ev.name to arrays
|
||||
|
const dataList = Array.isArray(ev.data) ? ev.data : [ev.data]; |
||||
|
const nameList = Array.isArray(ev.name) ? ev.name : [ev.name]; |
||||
|
for (let i = 0; i < dataList.length; i++) { |
||||
|
const base64String = dataList[i]; |
||||
|
const filename = nameList[i] || `file_${i}`; |
||||
|
fileDatas.push({ base64: base64String, filename: filename }); |
||||
|
} |
||||
|
|
||||
|
const resModel = this.props.record.resModel; |
||||
|
const resId = this.props.record.resId; |
||||
|
const resField = this.props.name; |
||||
|
const childField = this.props.childImageField || "image_1920"; |
||||
|
|
||||
|
await this.orm.call("ir.attachment", "action_save_drag_and_drop_images", [ |
||||
|
resModel, |
||||
|
resId, |
||||
|
resField, |
||||
|
childField, |
||||
|
fileDatas, |
||||
|
extraData, |
||||
|
]); |
||||
|
|
||||
|
await this.props.record.load(); |
||||
|
await this.props.record.model.notify(); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error("Error during file upload:", error); |
||||
|
this.notification.add( |
||||
|
_t("Error uploading images. Check logs or required fields."), |
||||
|
{ type: "danger" } |
||||
|
); |
||||
|
} finally { |
||||
|
this.isUploading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Remove a single tile (child record)
|
||||
|
removeRecord = async (recordId) => { |
||||
|
try { |
||||
|
const commands = [[3, recordId]]; |
||||
|
await this.orm.call(this.props.record.resModel, "write", [ |
||||
|
this.props.record.resId, |
||||
|
{ [this.props.name]: commands }, |
||||
|
]); |
||||
|
await this.props.record.load(); |
||||
|
this.notification.add("Image removed successfully.", { type: "success" }); |
||||
|
} catch (error) { |
||||
|
console.error("Error removing image:", error); |
||||
|
this.notification.add("Failed to remove image.", { type: "danger" }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Open a blank “new child” form in a modal
|
||||
|
async openChildFormView() { |
||||
|
const childModel = this.props.record.data[this.props.name].resModel; |
||||
|
const inverseField = this.props.record.fields[this.props.name].relation_field; |
||||
|
const ctx = { |
||||
|
[`default_${inverseField}`]: this.props.record.resId, |
||||
|
default_parent_id: this.props.record.resId, |
||||
|
}; |
||||
|
try { |
||||
|
const action = { |
||||
|
type: "ir.actions.act_window", |
||||
|
res_model: childModel, |
||||
|
views: [[false, "form"]], |
||||
|
target: "new", |
||||
|
context: ctx, |
||||
|
}; |
||||
|
await this.action.doAction(action, { |
||||
|
onClose: async () => { |
||||
|
await this.props.record.load(); |
||||
|
this.props.record.model.notify(); |
||||
|
}, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.error("Error opening child form view:", error); |
||||
|
this.notification.add(_t("Error opening child form view."), { type: "danger" }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
computeImageUrl(line) { |
||||
|
const model = this.props.record.data[this.props.name].resModel; |
||||
|
const field = this.props.extraData.previewImage || this.props.childImageField; |
||||
|
return `/web/image?model=${model}&id=${line.data.id}&field=${field}&unique=${line.data.write_date}`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const dndImagesField = { |
||||
|
component: DndImagesWidget, |
||||
|
displayName: _t("Enhanced DnD Multi Images"), |
||||
|
supportedTypes: ["one2many"], |
||||
|
extractProps: ({ attrs, options }) => { |
||||
|
return { |
||||
|
childImageField: options.childImageField || "image_1920", |
||||
|
extraData: options.extraData || {}, |
||||
|
}; |
||||
|
}, |
||||
|
}; |
||||
|
registry.category("fields").add("d_and_d_images", dndImagesField); |
@ -0,0 +1,192 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<templates xml:space="preserve"> |
||||
|
<!-- dnd_widgets_templates.xml --> |
||||
|
<t t-name="eis_drag_drop.DndImageWidgetTemplate"> |
||||
|
<div class="dnd_widget_container"> |
||||
|
<!-- Drop Zone --> |
||||
|
<div class="dnd_drop_zone" |
||||
|
t-ref="dropZoneRef" |
||||
|
t-att-class="{ |
||||
|
'dnd_active_drag': isDraggingOver.value, |
||||
|
'dnd_no_image': !imageUrl |
||||
|
}" |
||||
|
t-on-drop="onDrop" |
||||
|
t-on-dragover="onDragOver" |
||||
|
t-on-dragenter="onDragEnter" |
||||
|
t-on-dragleave="onDragLeave" |
||||
|
> |
||||
|
<!-- Controls (edit, remove, download) --> |
||||
|
<t t-if="imageUrl && !props.readonly"> |
||||
|
<div class="dnd_image_controls"> |
||||
|
<!-- Edit Button (Top Left) --> |
||||
|
<FileUploader onUploaded.bind="onFileUploaded" |
||||
|
acceptedFileExtensions="props.acceptedFileExtensions"> |
||||
|
<t t-set-slot="toggler"> |
||||
|
<button class="dnd_button dnd_edit_button" type="button"> |
||||
|
<i class="fa fa-pencil"/> |
||||
|
</button> |
||||
|
</t> |
||||
|
</FileUploader> |
||||
|
|
||||
|
<!-- Upload Button (Top Right) --> |
||||
|
<button t-if="props.record.data[props.name]" |
||||
|
class="dnd_button dnd_upload_button" |
||||
|
type="button" |
||||
|
t-on-click="onFileUploaded"> |
||||
|
<i class="fa fa-upload"/> |
||||
|
</button> |
||||
|
|
||||
|
<!-- Remove Button (Bottom Left) --> |
||||
|
<button t-if="props.record.data[props.name]" |
||||
|
class="dnd_button dnd_remove_button" |
||||
|
type="button" |
||||
|
t-on-click="onFileRemove"> |
||||
|
<i class="fa fa-trash-o"/> |
||||
|
</button> |
||||
|
</div> |
||||
|
</t> |
||||
|
|
||||
|
<!-- Image Preview --> |
||||
|
<img t-if="imageUrl" |
||||
|
loading="lazy" |
||||
|
t-att-src="imageUrl" |
||||
|
class="dnd_image_preview" |
||||
|
alt="Drag & Drop" |
||||
|
t-att-style="imageStyle" |
||||
|
t-on-mouseenter="onMouseEnter" |
||||
|
t-on-mouseleave="onMouseLeave" |
||||
|
t-on-click="onClickImage" |
||||
|
t-on-error="onLoadFailed" |
||||
|
/> |
||||
|
|
||||
|
<!-- Popup for zooming --> |
||||
|
<div class="dnd_zoom_popup" |
||||
|
t-att-class="{ show: showZoomPopup.value }" |
||||
|
t-on-mouseenter="onMouseEnterPopup" |
||||
|
t-on-mouseleave="onMouseLeavePopup"> |
||||
|
<a t-att-href="originalImageUrl" target="_blank" rel="noopener noreferrer"> |
||||
|
<img t-att-src="originalImageUrl" class="dnd_zoomed_image"/> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Placeholder if no image is available --> |
||||
|
<t t-if="!imageUrl"> |
||||
|
<div class="dnd_image_placeholder"> |
||||
|
<i class="fa fa-image"></i> |
||||
|
<p>Drag & Drop Image Here</p> |
||||
|
</div> |
||||
|
</t> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Uploading Spinner --> |
||||
|
<t t-if="isUploading.value"> |
||||
|
<div class="dnd_uploading_spinner"> |
||||
|
<i class="fa fa-spinner fa-spin"/> Uploading... |
||||
|
</div> |
||||
|
</t> |
||||
|
</div> |
||||
|
</t> |
||||
|
|
||||
|
|
||||
|
<!-- template for multiple images --> |
||||
|
<t t-name="eis_drag_drop.DndImagesWidgetTemplate"> |
||||
|
<div class="o_dnd_images_widget"> |
||||
|
<!-- Drag-and-drop container --> |
||||
|
<div t-att-class="{ |
||||
|
'o_dnd_drop_zone': true, |
||||
|
'o_dnd_dragging': isDragging.value |
||||
|
}" |
||||
|
t-ref="dropZoneRef" |
||||
|
t-on-drop.prevent="onDrop" |
||||
|
t-on-dragover="onDragOver" |
||||
|
t-on-dragenter="onDragEnter" |
||||
|
t-on-dragleave="onDragLeave" |
||||
|
style="width: 100%;"> |
||||
|
|
||||
|
<!-- Handle empty state with placeholder text --> |
||||
|
<t t-if="(props.record.data[props.name].records or []).length === 0"> |
||||
|
<!-- Manual Upload Tile --> |
||||
|
<div class="o_dnd_kanban_container"> |
||||
|
<div class="o_image_tile_upload"> |
||||
|
<FileUploader onUploaded.bind="onFileUploaded" multiUpload="true" |
||||
|
acceptedFileExtensions="'.bmp, .jpg, .jpeg, .png, .svg, .webp'"> |
||||
|
<div class="o_image_tile_inner"> |
||||
|
<t t-set-slot="toggler"> |
||||
|
<input type="text" class="o_input" t-att-value="fileName" readonly="readonly"/> |
||||
|
<button |
||||
|
class="btn btn-link btn-sm lh-1 fa fa-pencil o_select_file_button" |
||||
|
data-tooltip="Add" |
||||
|
aria-label="Add" |
||||
|
> Drag or Upload |
||||
|
</button> |
||||
|
</t> |
||||
|
</div> |
||||
|
</FileUploader> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
class="btn btn-secondary my-2" |
||||
|
t-on-click="openChildFormView"> |
||||
|
Add Video |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
</t> |
||||
|
|
||||
|
<!-- Existing Image Tiles --> |
||||
|
<div t-else="" class="o_dnd_kanban_container"> |
||||
|
<t t-set="childLines" t-value="props.record.data[props.name].records or []"/> |
||||
|
<t t-set="childModel" t-value="props.record.data[props.name].resModel"/> |
||||
|
|
||||
|
<!-- Display existing images as tiles --> |
||||
|
<t t-foreach="childLines" t-as="line" t-key="line.data.id"> |
||||
|
<div class="o_dnd_kanban_card"> |
||||
|
<img t-att-src="computeImageUrl(line)" t-att-alt="line.data.name" |
||||
|
t-attf-style="object-fit:cover; #{props.extraData.cssStyles || 'width:100px; height:100px' }" |
||||
|
class="o_dnd_kanban_image "/> |
||||
|
|
||||
|
<button type="button" class="close-button" |
||||
|
t-on-click.prevent="() => removeRecord(line.data.id)"> |
||||
|
X |
||||
|
</button> |
||||
|
</div> |
||||
|
</t> |
||||
|
|
||||
|
<!-- Manual Upload Tile --> |
||||
|
<div class="o_image_tile_upload"> |
||||
|
<FileUploader onUploaded.bind="onFileUploaded" multiUpload="true" |
||||
|
acceptedFileExtensions="'.bmp, .jpg, .jpeg, .png, .svg, .webp'"> |
||||
|
<div class="o_image_tile_inner"> |
||||
|
<t t-set-slot="toggler"> |
||||
|
<input type="text" class="o_input" t-att-value="fileName" readonly="readonly"/> |
||||
|
<button |
||||
|
class="btn btn-link btn-sm lh-1 fa fa-pencil o_select_file_button" |
||||
|
data-tooltip="Add" |
||||
|
aria-label="Add" |
||||
|
> Drag or Upload |
||||
|
</button> |
||||
|
</t> |
||||
|
</div> |
||||
|
</FileUploader> |
||||
|
</div> |
||||
|
<!-- Button to open form view in popup --> |
||||
|
<button type="button" |
||||
|
class="btn btn-secondary my-2" |
||||
|
t-on-click="openChildFormView"> |
||||
|
Add Video |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<!-- Upload in Progress Indicator --> |
||||
|
<t t-if="isUploading.value"> |
||||
|
<div class="o_dnd_uploading"> |
||||
|
<i class="fa fa-spinner fa-spin"></i> Uploading... |
||||
|
</div> |
||||
|
</t> |
||||
|
|
||||
|
</div> |
||||
|
</t> |
||||
|
|
||||
|
|
||||
|
</templates> |
@ -0,0 +1,46 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
<data> |
||||
|
|
||||
|
|
||||
|
<!-- Inherit Form View to Modify it --> |
||||
|
<record id="inherit_product_template_common" model="ir.ui.view"> |
||||
|
<field name="name">inherit.product.template.common.faheem</field> |
||||
|
<field name="model">product.template</field> |
||||
|
<field name="inherit_id" ref="product.product_template_form_view"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//field[@name='image_1920']" position="replace"> |
||||
|
<field name="image_1920" widget="d_and_d_image" class="oe_avatar" |
||||
|
options="{'image_size': '100x100', 'preview_image': 'image_128'}"/> |
||||
|
<!-- Add your fields or attributes here --> |
||||
|
</xpath> |
||||
|
<xpath expr="//field[@name='product_template_image_ids']" position="replace"> |
||||
|
<field name="product_template_image_ids" nolabel="1" widget="d_and_d_images" options="{ |
||||
|
'childImageField': 'image_1920', |
||||
|
'extraData': { |
||||
|
'previewImage': 'image_128', |
||||
|
}}"/> |
||||
|
|
||||
|
|
||||
|
<!-- Add your fields or attributes here --> |
||||
|
</xpath> |
||||
|
|
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<!-- Inherit Kanban View of odoo 18 to add missing id and name fields in it --> |
||||
|
<!-- <record id="product_image_kanban_view_inherit" model="ir.ui.view"> |
||||
|
<field name="name">product.view.image.kanban.inherit</field> |
||||
|
<field name="model">product.image</field> |
||||
|
<field name="inherit_id" ref="website_sale.product_image_view_kanban"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="video_url" position="after"> |
||||
|
<field name="id" invisible="1"/> |
||||
|
<field name="name" invisible="1"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> --> |
||||
|
|
||||
|
|
||||
|
</data> |
||||
|
</odoo> |