Browse Source

eis_drag_drop: added drag&drop module from Expert IT Solutions

17.0
ieva 2 months ago
commit
b2dd3ffb1c
  1. 2
      eis_drag_drop/__init__.py
  2. 100
      eis_drag_drop/__manifest__.py
  3. 949
      eis_drag_drop/doc/index.rst
  4. 1
      eis_drag_drop/models/__init__.py
  5. 87
      eis_drag_drop/models/models.py
  6. BIN
      eis_drag_drop/static/description/banner.gif
  7. BIN
      eis_drag_drop/static/description/icon.png
  8. 1
      eis_drag_drop/static/description/img/hero.svg
  9. BIN
      eis_drag_drop/static/description/img/image1.png
  10. BIN
      eis_drag_drop/static/description/img/image2.png
  11. BIN
      eis_drag_drop/static/description/img/image3.png
  12. BIN
      eis_drag_drop/static/description/img/image4.png
  13. BIN
      eis_drag_drop/static/description/img/image5.png
  14. BIN
      eis_drag_drop/static/description/img/image6.png
  15. BIN
      eis_drag_drop/static/description/img/logo.png
  16. 1474
      eis_drag_drop/static/description/index.html
  17. 49
      eis_drag_drop/static/src/css/style.scss
  18. 249
      eis_drag_drop/static/src/css/styles.css
  19. 233
      eis_drag_drop/static/src/js/dnd_image_widget.js
  20. 254
      eis_drag_drop/static/src/js/dnd_images_widget.js
  21. 192
      eis_drag_drop/static/src/xml/dnd_widgets_templates.xml
  22. 46
      eis_drag_drop/views/product.xml

2
eis_drag_drop/__init__.py

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

100
eis_drag_drop/__manifest__.py

@ -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 modules 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,
}

949
eis_drag_drop/doc/index.rst

@ -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

1
eis_drag_drop/models/__init__.py

@ -0,0 +1 @@
from . import models

87
eis_drag_drop/models/models.py

@ -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

BIN
eis_drag_drop/static/description/banner.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

BIN
eis_drag_drop/static/description/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

1
eis_drag_drop/static/description/img/hero.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
eis_drag_drop/static/description/img/image1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
eis_drag_drop/static/description/img/image2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
eis_drag_drop/static/description/img/image3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
eis_drag_drop/static/description/img/image4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
eis_drag_drop/static/description/img/image5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
eis_drag_drop/static/description/img/image6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
eis_drag_drop/static/description/img/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

1474
eis_drag_drop/static/description/index.html

File diff suppressed because it is too large

49
eis_drag_drop/static/src/css/style.scss

@ -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;
}
}
}
}

249
eis_drag_drop/static/src/css/styles.css

@ -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 */

233
eis_drag_drop/static/src/js/dnd_image_widget.js

@ -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);

254
eis_drag_drop/static/src/js/dnd_images_widget.js

@ -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);

192
eis_drag_drop/static/src/xml/dnd_widgets_templates.xml

@ -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 &amp;&amp; !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 &amp; 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 &amp; 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>

46
eis_drag_drop/views/product.xml

@ -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>
Loading…
Cancel
Save