You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1474 lines
64 KiB
1474 lines
64 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>eis_drag_drop - Drag And Drop Images Module Documentation</title>
|
|
<!-- Bootstrap CSS -->
|
|
<link
|
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
rel="stylesheet"
|
|
/>
|
|
<!-- Font Awesome -->
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
|
/>
|
|
<style>
|
|
|
|
/* Top Bar */
|
|
.top-bar {
|
|
background-color: #0914e9;
|
|
padding: 10px 20px;
|
|
color: #fff;
|
|
}
|
|
|
|
.top-bar img {
|
|
height: 40px;
|
|
}
|
|
|
|
.availability {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.availability .fa-check {
|
|
color: #28a745;
|
|
}
|
|
|
|
.availability .fa-times {
|
|
color: #dc3545;
|
|
}
|
|
|
|
/* Hero Section */
|
|
.hero-section {
|
|
background: linear-gradient(135deg, #5e8694bc, #adb5bd);
|
|
color: #fff;
|
|
padding: 80px 20px;
|
|
text-align: center;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
margin-bottom: 30px;
|
|
position: relative;
|
|
min-height: 700px;
|
|
height: 700px;
|
|
}
|
|
|
|
.hero-section .vector-placeholder {
|
|
width: 500px;
|
|
height: 350px;
|
|
/* background-color: rgba(255,255,255,0.2); */
|
|
margin: 20px auto;
|
|
/* border-radius: 50%; */
|
|
display: flex;
|
|
/* align-items: baseline; */
|
|
justify-content: center;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
}
|
|
|
|
.hero-section .benefit-box {
|
|
background-color: rgba(69, 205, 71, 0.3);
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
display: inline-block;
|
|
margin-top: 20px;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
/* Section Styling */
|
|
.section {
|
|
padding: 40px 20px;
|
|
background-color: #fff;
|
|
margin-bottom: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.section h2 {
|
|
border-bottom: 2px solid #dee2e6;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.section h3 {
|
|
margin-top: 30px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.section p,
|
|
.section li {
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Code Block Styling */
|
|
.code-snippet {
|
|
background-color: #f1f1f1;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
font-family: Consolas, Monaco, monospace;
|
|
position: relative;
|
|
margin-bottom: 20px;
|
|
border-left: 5px solid #007bff;
|
|
}
|
|
|
|
.code-snippet .highlight-field {
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
background-color: #fff3cd;
|
|
padding: 0 4px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Full-width Screenshot Styling */
|
|
.screenshot-full {
|
|
width: 100%;
|
|
margin-bottom: 40px;
|
|
text-align: center;
|
|
}
|
|
|
|
.screenshot-full .screenshot-img {
|
|
width: 100%;
|
|
height: auto;
|
|
border: 2px dashed #ced4da;
|
|
border-radius: 5px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.screenshot-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Module Version Info */
|
|
.version-info {
|
|
background-color: #e9ecef;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.version-info ul {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
}
|
|
|
|
.version-info li::before {
|
|
content: "\2022";
|
|
color: #007bff;
|
|
font-weight: bold;
|
|
display: inline-block;
|
|
width: 1em;
|
|
margin-left: -1em;
|
|
}
|
|
|
|
/* Services Tiles */
|
|
.service-tile {
|
|
background-color: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.service-tile:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.service-tile i {
|
|
font-size: 2rem;
|
|
color: #007bff;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Other Modules Slider */
|
|
.module-slider img {
|
|
width: 100%;
|
|
height: auto;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
/* Best Practices & Troubleshooting */
|
|
.tips {
|
|
background-color: #fff3cd;
|
|
border-left: 5px solid #ffc107;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.troubleshooting {
|
|
background-color: #f8d7da;
|
|
border-left: 5px solid #dc3545;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.video-container {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-bottom: 56.25%;
|
|
/* 16:9 aspect ratio */
|
|
height: 0;
|
|
}
|
|
|
|
.video-container iframe {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Top Bar -->
|
|
<div class="top-bar d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<img src="img/logo.png" alt="Logo"/>
|
|
</div>
|
|
<div class="availability">
|
|
<span>
|
|
<i class="fa fa-check"></i>
|
|
Community
|
|
</span>
|
|
|
|
<span>
|
|
<i class="fa fa-check"></i>
|
|
Enterprise
|
|
</span>
|
|
|
|
<span>
|
|
<i class="fa fa-times"></i>
|
|
Odoo Online
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hero Section -->
|
|
<section class="hero-section">
|
|
<div class="container">
|
|
<h1>Images Drag And Drop Widgets</h1>
|
|
<p class="lead">
|
|
Effortlessly manage and update images in your Odoo records with our
|
|
intuitive drag and drop widgets.
|
|
</p>
|
|
<div class="vector-placeholder">
|
|
<img src="img/hero.svg" alt="Hero Vector"/>
|
|
</div>
|
|
<div class="benefit-box">
|
|
<i class="fa fa-users"></i>
|
|
Perfect for E-commerce, Media Portals
|
|
& More!
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Introduction -->
|
|
<section class="section" id="introduction">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i
|
|
class="fa fa-envelope-open-text feature-icon"
|
|
aria-hidden="true"
|
|
></i
|
|
>
|
|
<br/>
|
|
Introduction
|
|
</h2>
|
|
<p>
|
|
The
|
|
<strong>Images Drag And Drop Widgets</strong>
|
|
module is a
|
|
state-of-the-art solution designed to enhance your Odoo experience by
|
|
integrating modern, user-friendly drag and drop image widgets.
|
|
</p>
|
|
<h3>Who Can Benefit?</h3>
|
|
<p>
|
|
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:
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
<strong>E-commerce websites:</strong>
|
|
Easily manage product images
|
|
with a simple drag and drop interface.
|
|
</li>
|
|
<li>
|
|
<strong>Businesses with heavy media content:</strong>
|
|
Quickly update
|
|
portfolios, galleries, and listings.
|
|
</li>
|
|
<li>
|
|
<strong>Developers and Designers:</strong>
|
|
Seamlessly integrate
|
|
modern widgets across all modules—no extra dependency required!
|
|
</li>
|
|
</ul>
|
|
<h3>Use Cases</h3>
|
|
<p>
|
|
<strong>Online Stores:</strong>
|
|
Showcase products with multiple images
|
|
that can be easily updated on-the-fly.
|
|
</p>
|
|
<p>
|
|
<strong>Real Estate Platforms:</strong>
|
|
Effortlessly manage property
|
|
photos, floor plans, and virtual tours.
|
|
</p>
|
|
<p>
|
|
<strong>Agencies & Portfolios:</strong>
|
|
Create visually appealing
|
|
portfolios where media speaks louder than words.
|
|
</p>
|
|
<h3>Benefits</h3>
|
|
<p>
|
|
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!
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Installation -->
|
|
<section class="section" id="installation">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-tools feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
Installation
|
|
</h2>
|
|
<p>
|
|
Follow these detailed steps to install the
|
|
<strong>eis_drag_drop</strong>
|
|
module:
|
|
</p>
|
|
<ol>
|
|
<li>
|
|
<strong>Download the Module:</strong>
|
|
Obtain the module as a ZIP
|
|
file from the official repository or your vendor.
|
|
</li>
|
|
<li>
|
|
<strong>Extract the ZIP:</strong>
|
|
Unzip the downloaded file to
|
|
reveal the module folder.
|
|
</li>
|
|
<li>
|
|
<strong>Copy to Addons Folder:</strong>
|
|
Move or copy the extracted
|
|
module folder into your Odoo
|
|
<code>addons</code>
|
|
directory.
|
|
</li>
|
|
<li>
|
|
<strong>Restart Odoo Service:</strong>
|
|
<ul>
|
|
<li>
|
|
On systemd-based systems:
|
|
<code>sudo systemctl restart odoo</code>
|
|
</li>
|
|
<li>
|
|
On SysVinit-based systems:
|
|
<code>sudo service odoo restart</code>
|
|
</li>
|
|
<li>Or use the appropriate command for your OS.</li>
|
|
</ul>
|
|
</li>
|
|
<li>
|
|
<strong>Update App List:</strong>
|
|
In your Odoo database, navigate to
|
|
the Apps menu and click <em>Update Apps List</em>.
|
|
</li>
|
|
<li>
|
|
<strong>Install the Module:</strong>
|
|
Search for
|
|
<em>Drag and Drop Widgets</em>
|
|
(or <code>eis_drag_drop</code>) and
|
|
click install.
|
|
</li>
|
|
</ol>
|
|
<div class="tips">
|
|
<strong>Important:</strong>
|
|
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.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- User Guides -->
|
|
<section class="section" id="user-guidesa">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-map-signs feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
User Guide
|
|
</h2>
|
|
|
|
<h3>
|
|
<i class="fa fa-desktop"></i>
|
|
Updating Existing Image Fields
|
|
</h3>
|
|
<p>
|
|
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:
|
|
</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
<p>
|
|
<strong>Options Explained:</strong>
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
<code>image_size</code>: Defines the display size of the widget.
|
|
Example: "150x150" sets the width to 150px and height to 150px.
|
|
</li>
|
|
<li>
|
|
<code>preview_image</code>: Specifies the field used for the image
|
|
preview. If not set, it defaults to the binary field name.
|
|
</li>
|
|
<li>
|
|
<code>acceptedFileExtensions</code>: Determines which image formats
|
|
can be uploaded.
|
|
</li>
|
|
<li>
|
|
<code>enableZoom</code>: If true, hovering over the image displays a
|
|
zoom popup.
|
|
</li>
|
|
<li>
|
|
<code>additionalStyles</code>: Custom CSS styles that are appended
|
|
to the widget's inline styles.
|
|
</li>
|
|
</ul>
|
|
|
|
<h3>
|
|
<i class="fa fa-code"></i>
|
|
Detailed O2M Setup & Custom Integration
|
|
</h3>
|
|
<p>
|
|
For One2Many fields, our widget transforms the standard image upload
|
|
into a dynamic drag and drop area.
|
|
<strong>Note:</strong>
|
|
The related model
|
|
<em>must</em>
|
|
have a Kanban
|
|
view defined that includes at least the
|
|
<code>name</code>
|
|
and
|
|
<code>image</code>
|
|
(or <code>image_1920</code>) fields.
|
|
</p>
|
|
<p>
|
|
<strong>Why a Kanban View?</strong>
|
|
The Kanban view enables the widget
|
|
to render image previews instead of just record IDs. Without it, you
|
|
may only see numerical identifiers.
|
|
</p>
|
|
<p>
|
|
<strong>Form View for Videos & Manual Entry:</strong>
|
|
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.
|
|
</p>
|
|
|
|
<h4>Example of One2Many Field Integration</h4>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
|
|
<h4>Options Explained:</h4>
|
|
<ul>
|
|
<li>
|
|
<code>childImageField</code>: 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.
|
|
</li>
|
|
<li>
|
|
<code>extraData</code>: Allows you to pass additional data to the
|
|
child records. This can include:
|
|
<ul>
|
|
<li>
|
|
<code>categ_id</code>: 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.
|
|
</li>
|
|
<li>
|
|
<code>sale_ok</code>: This is another example field and you can
|
|
pass any bool value if your required field is bool type
|
|
</li>
|
|
<li>
|
|
<code>previewImage</code>: 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
|
|
<code>image.mixin</code>
|
|
and you
|
|
want to display a smaller preview image.
|
|
</li>
|
|
<li>
|
|
<code>cssStyles</code>: Custom CSS styles for the image display.
|
|
Example:
|
|
<code
|
|
>"width:150px; height:200px; border-radius:15px;
|
|
object-fit:cover;"</code
|
|
>.
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li>
|
|
<code>acceptedFileExtensions</code>: Determines which file types are
|
|
acceptable for upload. Default is <code>"image/*"</code>.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- User Guides -->
|
|
<section class="section" id="user-guides">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-map-signs feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
User Guide
|
|
</h2>
|
|
|
|
<h3>
|
|
<i class="fa fa-desktop"></i>
|
|
Updating Existing Image Fields
|
|
</h3>
|
|
<p>
|
|
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:
|
|
</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
<p>
|
|
<strong>Options Explained:</strong>
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
<code>image_size</code>: Defines the display size of the widget.
|
|
Example: "150x150" sets the width to 150px and height to 150px.
|
|
</li>
|
|
<li>
|
|
<code>preview_image</code>: Specifies the field used for the image
|
|
preview. If not set, it defaults to the binary field name.
|
|
</li>
|
|
<li>
|
|
<code>acceptedFileExtensions</code>: Determines which image formats
|
|
can be uploaded.
|
|
</li>
|
|
<li>
|
|
<code>enableZoom</code>: If true, hovering over the image displays a
|
|
zoom popup.
|
|
</li>
|
|
<li>
|
|
<code>additionalStyles</code>: Custom CSS styles that are appended
|
|
to the widget's inline styles.
|
|
</li>
|
|
</ul>
|
|
|
|
<h3>
|
|
<i class="fa fa-code"></i>
|
|
Detailed O2M Setup & Custom Integration
|
|
</h3>
|
|
<p>
|
|
For One2Many fields, our widget transforms the standard image upload
|
|
into a dynamic drag and drop area.
|
|
<strong>Note:</strong>
|
|
The related
|
|
model
|
|
<em>must</em>
|
|
have a Kanban view defined that includes at least
|
|
the
|
|
<code>name</code>
|
|
and
|
|
<code>image</code>
|
|
(or
|
|
<code>image_1920</code>) fields.
|
|
</p>
|
|
<p>
|
|
<strong>Why a Kanban View?</strong>
|
|
The Kanban view enables the widget
|
|
to render image previews instead of just record IDs. Without it, you
|
|
may only see numerical identifiers.
|
|
</p>
|
|
<p>
|
|
<strong>Form View for Videos & Manual Entry:</strong>
|
|
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.
|
|
</p>
|
|
|
|
<h4>Example of One2Many Field Integration</h4>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
|
|
<h4>Options Explained:</h4>
|
|
<ul>
|
|
<li>
|
|
<code>childImageField</code>: 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,
|
|
<code>image_1920</code>
|
|
will be used.
|
|
</li>
|
|
<li>
|
|
<code>extraData</code>: Allows you to pass additional data to the
|
|
child records. This can include:
|
|
<ul>
|
|
<li>
|
|
<code>categ_id</code>: 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.
|
|
</li>
|
|
<li>
|
|
<code>sale_ok</code>: This is another example field and you can
|
|
pass any bool value if your required field is bool type.
|
|
</li>
|
|
<li>
|
|
<code>previewImage</code>: 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
|
|
<code>image.mixin</code>
|
|
and you
|
|
want to display a smaller preview image.
|
|
</li>
|
|
<li>
|
|
<code>cssStyles</code>: Custom CSS styles for the image display.
|
|
Example:
|
|
<code
|
|
>"width:150px; height:200px; border-radius:15px;
|
|
object-fit:cover;"</code
|
|
>.
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li>
|
|
<code>acceptedFileExtensions</code>: Determines which file types are
|
|
acceptable for upload. Default is <code>"image/*"</code>.
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
<div class="alert alert-danger" role="alert">
|
|
<h4 class="alert-heading">
|
|
<i class="fa fa-warning feature-icon fa-2x" aria-hidden="true"></i>
|
|
Important Notice!
|
|
</h4>
|
|
<p>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.
|
|
</p>
|
|
</div>
|
|
<br/>
|
|
<h4>
|
|
Complete Example (Taken from Odoo builtin Code):
|
|
<br/>
|
|
</h4>
|
|
<h5>Adding Multiple Image Support to
|
|
<code>product.template</code>
|
|
</h5>
|
|
<p>
|
|
Let's say we have a model
|
|
<code>product.template</code>
|
|
and we need to
|
|
add multiple image support to it. We will create a model
|
|
<code>product.image</code>
|
|
by inheriting from the
|
|
<code>image.mixin</code>
|
|
class. This way, we automatically get fields
|
|
like
|
|
<code>image_1920</code>
|
|
and other related fields for our model
|
|
<code>product.image</code>.
|
|
</p>
|
|
<p>Here are the steps to achieve this:</p>
|
|
<ol>
|
|
<li>
|
|
<strong>Create the
|
|
<code>product.image</code>
|
|
Model:
|
|
</strong>
|
|
<p>
|
|
Inherit from
|
|
<code>image.mixin</code>
|
|
to automatically get
|
|
image-related fields.
|
|
</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code>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')
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<strong>Extend the
|
|
<code>product.template</code>
|
|
Model:
|
|
</strong>
|
|
<p>Add a One2Many field to link multiple images.</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code>class ProductTemplate(models.Model):
|
|
_inherit = 'product.template'
|
|
|
|
product_template_image_ids = fields.One2many('product.image', 'product_tmpl_id',
|
|
string='Images')
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<strong
|
|
>Create a Kanban View for <code>product.image</code>:
|
|
</strong
|
|
>
|
|
<p>Add a Kanban view with the necessary fields.</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<strong>Create a Form View for <code>product.image</code>:</strong>
|
|
<p>Add a form view with fields for video URL and image.</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<strong>Update the
|
|
<code>product.template</code>
|
|
Form View:
|
|
</strong>
|
|
<p>Add the One2Many field with the drag-and-drop widget.</p>
|
|
<div class="code-snippet">
|
|
<pre>
|
|
<code><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>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<div class="alert alert-warning">
|
|
<strong>Important Considerations for Product Image Management (Odoo 17 and 18)</strong>
|
|
|
|
<p>This guide outlines the necessary steps for integrating d_and_d_images widget for product
|
|
template, specifically when using the
|
|
<code>website_sale</code>
|
|
module, across Odoo 17 and 18. Please carefully follow the instructions for your respective Odoo
|
|
version to avoid conflicts and ensure proper functionality.
|
|
</p>
|
|
|
|
<p>
|
|
<strong>Odoo 17:</strong>
|
|
<ol>
|
|
<li>
|
|
<strong>Do not duplicate the
|
|
<code>product.image</code>
|
|
model or its views.
|
|
</strong>
|
|
This model is already provided by the
|
|
<code>website_sale</code>
|
|
module in Odoo. Creating a duplicate will lead to conflicts.
|
|
</li>
|
|
<li>
|
|
<strong>Inherit the
|
|
<code>product.template</code>
|
|
form view.
|
|
</strong>
|
|
. You will modify the form view
|
|
<code>product.product_template_form_view</code>
|
|
to integrate d_and_d_images customizations seamlessly with Odoo's core functionality.
|
|
</li>
|
|
<li>
|
|
<strong>Customize the image field:</strong>
|
|
Replace the
|
|
<code>product_template_image_ids</code>
|
|
field in the inherited form view with d_and_d_images widget as described above to enhance
|
|
the product image management functionality.
|
|
</li>
|
|
</ol>
|
|
</p>
|
|
|
|
<p>
|
|
<strong>Odoo 18:</strong>
|
|
<p>Odoo 18 has modified the
|
|
<code>product.image</code>
|
|
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:
|
|
</p>
|
|
<ol>
|
|
<li>
|
|
<strong>Inherit the
|
|
<code>product.image</code>
|
|
Kanban view.
|
|
</strong>
|
|
<code>website_sale.product_image_view_kanban</code>. Add the necessary
|
|
<code>id</code>
|
|
and
|
|
<code>name</code>
|
|
fields back into the view to maintain existing functionality. That's it.
|
|
</li>
|
|
<br/>
|
|
<li>
|
|
<strong>Enable modifications through the module's manifest:</strong>
|
|
<ul>
|
|
<li>Declare dependency on
|
|
<code>website_sale</code>
|
|
in our eis_drag_drop module's
|
|
<code>__manifest__.py</code>
|
|
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.
|
|
</li>
|
|
<li>Reference the XML file containing your Kanban view modifications in the
|
|
<code>data</code>
|
|
key of the manifest to apply these changes automatically when your module is
|
|
installed or updated.
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li>
|
|
<strong>Alternative approach:</strong>
|
|
Manually define the Kanban view inheritance in a
|
|
<code>product.xml</code>
|
|
file if you prefer more direct control over the modifications.
|
|
</li>
|
|
</ol>
|
|
</p>
|
|
|
|
<p>Our module has pre-configured these enhancements for you. To activate them, modify the
|
|
<code>__manifest__.py</code>
|
|
file to uncomment the dependency on
|
|
<code>website_sale</code>
|
|
and the reference to
|
|
<code>/views/product.xml</code>
|
|
in the
|
|
<code>data</code>
|
|
key. This ensures all functionalities are seamlessly integrated when you install or update the
|
|
module.
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Screenshots -->
|
|
<section class="section" id="screenshots">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-images feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
Screenshots Explaination
|
|
</h2>
|
|
|
|
<!-- Screenshot 1 -->
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">01</div>
|
|
<h4>Drag and Drop Widget Overview</h4>
|
|
<img
|
|
src="img/image1.png"
|
|
alt="Drag and Drop Widget Overview"
|
|
class="screenshot-img"
|
|
/>
|
|
<p>
|
|
A clear view of the drag and drop widget areas for single binary
|
|
image and for one2many images., allowing users to effortlessly
|
|
update images.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Screenshot 3 -->
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">02</div>
|
|
<h4>One2Many Drag And Drop Explained</h4>
|
|
<img
|
|
src="img/image3.png"
|
|
alt="One2many Field Enhancement"
|
|
class="screenshot-img"
|
|
/>
|
|
<p>
|
|
In this screenshot, you can see a simple explaination 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.
|
|
</p>
|
|
</div>
|
|
<!-- Screenshot 4 -->
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">04</div>
|
|
<h4>Add Video Button</h4>
|
|
<img
|
|
src="img/image4.png"
|
|
alt="One2many Field Enhancement"
|
|
class="screenshot-img"
|
|
/>
|
|
<p>
|
|
This screenshot illustrates how the one2many field widget
|
|
<code>'Add Video'</code>
|
|
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.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Screenshot 5 -->
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">05</div>
|
|
<h4>Manual File Uploding Button Display</h4>
|
|
<img
|
|
src="img/image5.png"
|
|
alt="One2many Field Enhancement"
|
|
class="screenshot-img"
|
|
/>
|
|
<p>
|
|
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
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Screenshot 5 -->
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">06</div>
|
|
<h4>Iamges Dragging over One2many Field Display</h4>
|
|
<img
|
|
src="img/image6.png"
|
|
alt="One2many Field Enhancement"
|
|
class="screenshot-img"
|
|
/>
|
|
<p>
|
|
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
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="section" id="video">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-video feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
Video Explaination
|
|
</h2>
|
|
<div class="screenshot-full">
|
|
<div class="screenshot-number">01</div>
|
|
<h4>Drag and Drop Widget Overview</h4>
|
|
<!-- Responsive YouTube video embed -->
|
|
<div class="video-container">
|
|
<iframe
|
|
src="https://www.youtube.com/embed/JRPchiEhS9U?rel=0&controls=0&showinfo=0&iv_load_policy=3&disablekb=1&modestbranding=1"
|
|
title="Drag And Drop Widget - Odoo"
|
|
frameborder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowfullscreen
|
|
></iframe>
|
|
</div>
|
|
<p>Watch the video to see how it works.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Module Version Info -->
|
|
<section class="section" id="version-info">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-low-vision feature-icon" aria-hidden="true"></i>
|
|
<br/>
|
|
Module Version Information & Changelog
|
|
</h2>
|
|
|
|
<div class="version-info">
|
|
<ul>
|
|
<li>
|
|
<strong>v1.0.0</strong>
|
|
- Initial release with basic drag and drop
|
|
functionality.
|
|
</li>
|
|
<li>
|
|
<strong>v1.1.0</strong>
|
|
- Added support for one2many fields and
|
|
improved preview handling.
|
|
</li>
|
|
<li>
|
|
<strong>v1.2.0</strong>
|
|
- Enhanced UI, added zoom popup, and
|
|
refined installation instructions.
|
|
</li>
|
|
<li>
|
|
<strong>v1.3.0</strong>
|
|
- Bug fixes and performance improvements
|
|
for large image uploads.
|
|
</li>
|
|
<li>
|
|
|
|
<strong>v1.3.1</strong>
|
|
<ul>
|
|
<li> Fixes the situation where the form is dirty and d_and_d_images was discarding changes. in this version it will save the changes.
|
|
<li> A new option added 'showConfirm' in multi image widget d_and_d_images. It is bool
|
|
field and it will show a configuration dialog to ask for saving changes if form is
|
|
dirty. the extraData should contain a key 'showConfirm': true, if not provided or is false, the form will saved without confirmation.
|
|
</li>
|
|
<li>If not passed, default is to save dirty form to preserve changes.</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Services -->
|
|
<section class="section" id="services-2">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-hands-helping feature-icon" aria-hidden="true"></i
|
|
>
|
|
<br/>
|
|
90 Days Support
|
|
</h2>
|
|
<div class="row text-center">
|
|
<div class="col-md-4 mb-3">
|
|
<div class="service-tile">
|
|
<i class="fa fa-globe"></i>
|
|
<h5>Website</h5>
|
|
<p>
|
|
<a href="https://www.expertpk.com" target="_blank"
|
|
>www.expertpk.com
|
|
</a
|
|
>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<div class="service-tile">
|
|
<i class="fa fa-envelope"></i>
|
|
<h5>Email</h5>
|
|
<p>
|
|
<a href="mailto:support@expertpk.com">support@expertpk.com</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<div class="service-tile">
|
|
<i class="fa fa-whatsapp"></i>
|
|
<h5>WhatsApp</h5>
|
|
<p>+92 300 7888120</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Services Section -->
|
|
<section id="services" class="py-5 bg-light">
|
|
<div class="container">
|
|
<h2 class="text-center mb-5 fw-bold">
|
|
<i class="fa fa-handshake feature-icon"></i>
|
|
<br/>
|
|
Our Services
|
|
</h2>
|
|
<div class="row g-4">
|
|
<!-- Odoo Development -->
|
|
<div class="col-md-6 col-lg-3">
|
|
<div class="card h-100 text-center border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<i class="fa fa-code fa-3x text-primary mb-3"></i>
|
|
<h5 class="card-title">Odoo Development</h5>
|
|
<p class="card-text text-muted">
|
|
Build custom Odoo modules and applications tailored to your
|
|
business needs.
|
|
</p>
|
|
<a href="#contact" class="btn btn-outline-primary">
|
|
Learn More
|
|
<i class="fas fa-arrow-right ms-2"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customization -->
|
|
<div class="col-md-6 col-lg-3">
|
|
<div class="card h-100 text-center border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<i class="fa fa-tools fa-3x text-primary mb-3"></i>
|
|
<h5 class="card-title">Customization</h5>
|
|
<p class="card-text text-muted">
|
|
Customize Odoo to fit your unique workflows and business
|
|
processes.
|
|
</p>
|
|
<a href="#contact" class="btn btn-outline-primary">
|
|
Learn More
|
|
<i class="fa fa-arrow-right ms-2"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upgradation -->
|
|
<div class="col-md-6 col-lg-3">
|
|
<div class="card h-100 text-center border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<i class="fa fa-sync-alt fa-3x text-primary mb-3"></i>
|
|
<h5 class="card-title">Upgradation</h5>
|
|
<p class="card-text text-muted">
|
|
Upgrade your Odoo instance to the latest version with zero
|
|
downtime.
|
|
</p>
|
|
<a href="#contact" class="btn btn-outline-primary">
|
|
Learn More
|
|
<i class="fa fa-arrow-right ms-2"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Consultancy -->
|
|
<div class="col-md-6 col-lg-3">
|
|
<div class="card h-100 text-center border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<i
|
|
class="fa fa-chalkboard-teacher fa-3x text-primary mb-3"
|
|
></i>
|
|
<h5 class="card-title">Consultancy</h5>
|
|
<p class="card-text text-muted">
|
|
Get expert advice on Odoo implementation, optimization, and
|
|
best practices.
|
|
</p>
|
|
<a href="#contact" class="btn btn-outline-primary">
|
|
Learn More
|
|
<i class="fa fa-arrow-right ms-2"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Other Modules Slider -->
|
|
<section class="section" id="other-modules">
|
|
<div class="container">
|
|
<h2>Explore Our Other Modules</h2>
|
|
<div
|
|
id="modulesCarousel"
|
|
class="carousel slide module-slider"
|
|
data-bs-ride="carousel"
|
|
>
|
|
<div class="carousel-inner">
|
|
<div class="carousel-item active">
|
|
<img src="module1.png" class="d-block w-100" alt="Module 1"/>
|
|
<div class="carousel-caption d-none d-md-block">
|
|
<h5>Module 1: Advanced Reporting</h5>
|
|
</div>
|
|
</div>
|
|
<div class="carousel-item">
|
|
<img src="module2.png" class="d-block w-100" alt="Module 2"/>
|
|
<div class="carousel-caption d-none d-md-block">
|
|
<h5>Module 2: CRM Enhancements</h5>
|
|
</div>
|
|
</div>
|
|
<div class="carousel-item">
|
|
<img src="module3.png" class="d-block w-100" alt="Module 3"/>
|
|
<div class="carousel-caption d-none d-md-block">
|
|
<h5>Module 3: Inventory Optimizer</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="carousel-control-prev"
|
|
type="button"
|
|
data-bs-target="#modulesCarousel"
|
|
data-bs-slide="prev"
|
|
>
|
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Previous</span>
|
|
</button>
|
|
<button
|
|
class="carousel-control-next"
|
|
type="button"
|
|
data-bs-target="#modulesCarousel"
|
|
data-bs-slide="next"
|
|
>
|
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Next</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Best Practices & Troubleshooting -->
|
|
<section class="section" id="best-practices">
|
|
<div class="container">
|
|
<h2>Best Practices & Troubleshooting</h2>
|
|
<div class="tips">
|
|
<h5>Best Practices</h5>
|
|
<ul>
|
|
<li>Always backup your database before applying new modules.</li>
|
|
<li>
|
|
Test the module in a staging environment prior to production
|
|
deployment.
|
|
</li>
|
|
<li>
|
|
Ensure that the related one2many models have the required Kanban
|
|
view with mandatory fields.
|
|
</li>
|
|
<li>
|
|
Regularly update your Odoo instance to keep up with security and
|
|
performance improvements.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="troubleshooting">
|
|
<h5>Troubleshooting Steps</h5>
|
|
<ul>
|
|
<li>
|
|
If image previews are not displaying, verify that your Kanban view
|
|
contains the
|
|
<code>name</code>
|
|
and <code>image</code>/
|
|
<code
|
|
>image_1920
|
|
</code
|
|
>
|
|
fields.
|
|
</li>
|
|
<li>
|
|
Check the browser console for JavaScript errors related to the
|
|
widget.
|
|
</li>
|
|
<li>
|
|
Ensure file permissions are correctly set in your addons folder.
|
|
</li>
|
|
<li>
|
|
Review the server logs for any errors during file upload or
|
|
service restart.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Code Snippets Showcase -->
|
|
<section class="section" id="code-snippets">
|
|
<div class="container">
|
|
<h2>Code Snippets</h2>
|
|
<p>
|
|
Below are examples of typical XML view definitions where the new drag
|
|
and drop fields are highlighted:
|
|
</p>
|
|
|
|
<!-- Single Image Widget Example -->
|
|
<div class="code-snippet">
|
|
<h3>Single Image Widget</h3>
|
|
<p>
|
|
This snippet shows how to integrate a single image drag and drop
|
|
field into an Odoo form view:
|
|
</p>
|
|
<pre>
|
|
<code><form string="Product Form">
|
|
<sheet>
|
|
<group>
|
|
<field name="name" />
|
|
<field name="description" />
|
|
<!-- Our new drag and drop image field is highlighted below -->
|
|
<span class="highlight-field"> <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" />
|
|
</span>
|
|
</group>
|
|
</sheet>
|
|
</form>
|
|
</code>
|
|
</pre>
|
|
<p>
|
|
<strong>Explanation:</strong>
|
|
This code defines a form view for a
|
|
product. The
|
|
<code>product_image</code>
|
|
field uses the
|
|
<code>d_and_d_image</code>
|
|
widget, which allows users to drag and
|
|
drop images. The
|
|
<code>options</code>
|
|
attribute specifies settings
|
|
like image size, preview image, accepted file extensions, and zoom
|
|
functionality.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- One2Many Image Widget Example -->
|
|
<div class="code-snippet">
|
|
<h3>One2Many Image Widget</h3>
|
|
<p>
|
|
This snippet demonstrates how to integrate a One2Many image drag and
|
|
drop field into an Odoo form view:
|
|
</p>
|
|
<pre>
|
|
<code><form string="Product Form">
|
|
<sheet>
|
|
<group>
|
|
<field name="name" />
|
|
<field name="description" />
|
|
<!-- One2Many drag and drop image field -->
|
|
<span class="highlight-field"> <field name="product_images" widget="d_and_d_images"
|
|
options="{
|
|
"childImageField": "image_1920",
|
|
"extraData": {
|
|
"cssStyles": "width:150px;height:150px;"
|
|
}
|
|
}" />
|
|
<field name="price" />
|
|
</span>
|
|
</group>
|
|
</sheet>
|
|
</form>
|
|
</code>
|
|
</pre>
|
|
<p>
|
|
<strong>Explanation:</strong>
|
|
This code defines a form view for a
|
|
product with a One2Many field <code>product_images</code>. The
|
|
<code>d_and_d_images</code>
|
|
widget allows users to drag and drop
|
|
multiple images. The
|
|
<code>options</code>
|
|
attribute specifies the
|
|
child image field and additional CSS styles for the image display.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Footer -->
|
|
<footer class="text-center py-3">
|
|
<small>© 2025 Expert IT Solutions. All rights reserved.</small>
|
|
</footer>
|
|
|
|
<!-- Bootstrap JS Bundle -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|
|
|