summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorDharmraj Jhala <dja@odoo.com>2019-02-14 16:37:25 +0000
committerSébastien Theys <seb@odoo.com>2019-02-14 16:37:25 +0000
commit7daa85dd12ff522be510bc2f8db1d968cae7d891 (patch)
treeab1614d6e14d89be9329445c5428b3e3213938d4 /addons
parent413c63556f5c41b3c135fdcce0aa1adb177d9179 (diff)
[IMP] base,product,website_sale,*: improve product images
* = account, purchase, website_sale_comparison, website_sale_wishlist Raw Image & Mixin ================= Previously, the main image from a product was stored resized. This caused inconsistencies with the size of the images on ecommerce: the main image was resized, but the extra images were not. Now, we always store all raw images, thanks to the new mixin. This way, product images that were created before the installation of ecommerce will be ready to be used in ecommerce without any other change. We also store the resized versions instead of computing them on the fly: this will take more disk space, but significantly increase the speed of the pages when displaying images, as well as reduce server CPU load. Images on variants ================== Previously, it was only possible to set the extra product images on a template, and not on a variant. Now, we allow extra images also on the variants, this gives more flexibility to the user. Both extra images on template and variant have been given a sequence field to be able to sort them easily. On /shop we always display the image of the template for performance reasons. On /product we never display the image of the template, unless the variant has no image, then the template image is the fallback. Carousel ======== The carousel view has been cleaned and improved: - remove the duplicate XML that was created with the product configurator fix - use a single loop from a single source to get all the images, instead of merging the different sources in the view with complex conditions - improve the HTML structure, reduce the CSS with appropriate classes Preview Image ============= Both in website and backend: make better use of `preview_image` to display a small image whenever possible, to reduce download size for the end user. task-34045 PR: #30656 Co-authored-by: Parth Gajjar <pga@odoo.com> Co-authored-by: Sébastien Theys <seb@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_invoice.py1
-rw-r--r--addons/product/models/product.py77
-rw-r--r--addons/product/models/product_template.py20
-rw-r--r--addons/product/views/product_views.xml4
-rw-r--r--addons/purchase/models/purchase.py3
-rw-r--r--addons/sale/models/sale.py3
-rw-r--r--addons/website_sale/controllers/main.py8
-rw-r--r--addons/website_sale/data/mail_template_data.xml2
-rw-r--r--addons/website_sale/models/__init__.py1
-rw-r--r--addons/website_sale/models/product.py48
-rw-r--r--addons/website_sale/models/product_image.py19
-rw-r--r--addons/website_sale/static/src/js/website_sale.js31
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js6
-rw-r--r--addons/website_sale/static/src/scss/website_sale.scss60
-rw-r--r--addons/website_sale/static/src/scss/website_sale_backend.scss58
-rw-r--r--addons/website_sale/views/product_views.xml109
-rw-r--r--addons/website_sale/views/templates.xml92
-rw-r--r--addons/website_sale_comparison/views/website_sale_comparison_template.xml2
-rw-r--r--addons/website_sale_wishlist/views/website_sale_wishlist_template.xml2
19 files changed, 243 insertions, 303 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 67f482ba30d..2a9218ad65a 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1693,7 +1693,6 @@ class AccountInvoiceLine(models.Model):
ondelete='set null', index=True, oldname='uos_id')
product_id = fields.Many2one('product.product', string='Product',
ondelete='restrict', index=True)
- product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=True)
account_id = fields.Many2one('account.account', string='Account', domain=[('deprecated', '=', False)],
default=_default_account,
help="The income or expense account related to the selected product.")
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 7f37b7ee74c..5912a897a6d 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -83,7 +83,7 @@ class ProductProduct(models.Model):
_name = "product.product"
_description = "Product"
_inherits = {'product.template': 'product_tmpl_id'}
- _inherit = ['mail.thread', 'mail.activity.mixin']
+ _inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']
_order = 'default_code, name, id'
# price: total price, context dependent (partner, pricelist, quantity)
@@ -118,20 +118,6 @@ class ProductProduct(models.Model):
'product.attribute.value', string='Attribute Values', ondelete='restrict')
product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value', string='Template Attribute Values', compute="_compute_product_template_attribute_value_ids")
- # image: all image fields are base64 encoded and PIL-supported
- image_variant = fields.Binary(
- "Variant Image",
- help="This field holds the image used as image for the product variant, limited to 1024x1024px.")
- image = fields.Binary(
- "Big-sized image", compute='_compute_images', inverse='_set_image',
- help="Image of the product variant (Big-sized image of product template if false). It is automatically "
- "resized as a 1024x1024px image, with aspect ratio preserved.")
- image_small = fields.Binary(
- "Small-sized image", compute='_compute_images', inverse='_set_image_small',
- help="Image of the product variant (Small-sized image of product template if false).")
- image_medium = fields.Binary(
- "Medium-sized image", compute='_compute_images', inverse='_set_image_medium',
- help="Image of the product variant (Medium-sized image of product template if false).")
is_product_variant = fields.Boolean(compute='_compute_is_product_variant')
standard_price = fields.Float(
@@ -241,47 +227,6 @@ class ProductProduct(models.Model):
else:
self.partner_ref = self.display_name
- @api.one
- @api.depends('image_variant', 'product_tmpl_id.image')
- def _compute_images(self):
- if self._context.get('bin_size'):
- self.image_medium = self.image_variant
- self.image_small = self.image_variant
- self.image = self.image_variant
- else:
- resized_images = tools.image_get_resized_images(self.image_variant, return_big=True, avoid_resize_medium=True)
- self.image_medium = resized_images['image_medium']
- self.image_small = resized_images['image_small']
- self.image = resized_images['image']
- if not self.image_medium:
- self.image_medium = self.product_tmpl_id.image_medium
- if not self.image_small:
- self.image_small = self.product_tmpl_id.image_small
- if not self.image:
- self.image = self.product_tmpl_id.image
-
- @api.one
- def _set_image(self):
- self._set_image_value(self.image)
-
- @api.one
- def _set_image_medium(self):
- self._set_image_value(self.image_medium)
-
- @api.one
- def _set_image_small(self):
- self._set_image_value(self.image_small)
-
- @api.one
- def _set_image_value(self, value):
- if isinstance(value, str):
- value = value.encode('ascii')
- image = tools.image_resize_image_big(value)
- if self.product_tmpl_id.image:
- self.image_variant = image
- else:
- self.product_tmpl_id.image = image
-
@api.depends('product_tmpl_id', 'attribute_value_ids')
def _compute_product_template_attribute_value_ids(self):
# Fetch and pre-map the values first for performance. It assumes there
@@ -306,6 +251,26 @@ class ProductProduct(models.Model):
else:
product.product_template_attribute_value_ids += values_per_template[product.product_tmpl_id.id][pav.id]
+ @api.multi
+ def _get_image_fallback_record(self):
+ """Override to get the image from the template if no image is set on the
+ variant."""
+ return self.product_tmpl_id
+
+ @api.multi
+ def _force_write_image_on_fallback(self):
+ """Override to always write on the template if there is only one
+ variant.
+
+ This is needed because when there is only one variant, the user doesn't
+ know there is a difference between template and variant, he expects both
+ images to be the same.
+ """
+ return self.env['product.product'].search_count([
+ ('product_tmpl_id', '=', self.product_tmpl_id.id),
+ ('active', '=', True),
+ ]) <= 1
+
@api.one
def _get_pricelist_items(self):
self.pricelist_item_ids = self.env['product.pricelist.item'].search([
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index e50866e59b4..237fccdb2a2 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -12,7 +12,7 @@ from odoo.osv import expression
class ProductTemplate(models.Model):
_name = "product.template"
- _inherit = ['mail.thread', 'mail.activity.mixin']
+ _inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']
_description = "Product Template"
_order = "name"
@@ -134,20 +134,6 @@ class ProductTemplate(models.Model):
item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id', 'Pricelist Items')
- # image: all image fields are base64 encoded and PIL-supported
- image = fields.Binary(
- "Image", help="This field holds the image used as image for the product, limited to 1024x1024px.")
- image_medium = fields.Binary(
- "Medium-sized image",
- help="Medium-sized image of the product. It is automatically "
- "resized as a 128x128px image, with aspect ratio preserved, "
- "only when the image exceeds one of those sizes. Use this field in form views or some kanban views.")
- image_small = fields.Binary(
- "Small-sized image",
- help="Small-sized image of the product. It is automatically "
- "resized as a 64x64px image, with aspect ratio preserved. "
- "Use this field anywhere a small image is required.")
-
@api.depends('product_variant_ids')
def _compute_product_variant_id(self):
for p in self:
@@ -328,9 +314,6 @@ class ProductTemplate(models.Model):
@api.model_create_multi
def create(self, vals_list):
''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
- # TDE FIXME: context brol
- for vals in vals_list:
- tools.image_resize_images(vals)
templates = super(ProductTemplate, self).create(vals_list)
if "create_product_product" not in self._context:
templates.with_context(create_from_tmpl=True).create_variant_ids()
@@ -355,7 +338,6 @@ class ProductTemplate(models.Model):
@api.multi
def write(self, vals):
- tools.image_resize_images(vals)
res = super(ProductTemplate, self).write(vals)
if 'attribute_line_ids' in vals or vals.get('active'):
self.create_variant_ids()
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index 185002521a8..db053685d7e 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -26,7 +26,7 @@
options='{"terminology": "archive"}'/>
</button>
</div>
- <field name="image_medium" widget="image" class="oe_avatar"/>
+ <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
<div class="oe_title">
<label class="oe_edit_only" for="name" string="Product Name"/>
<h1><field name="name" placeholder="Product Name"/></h1>
@@ -275,7 +275,7 @@
</button>
</div>
<field name="id" invisible="1"/>
- <field name="image_medium" widget="image" class="oe_avatar"/>
+ <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
<div class="oe_title">
<label class="oe_edit_only" for="name" string="Product Name"/>
<h1><field name="name" readonly="1" placeholder="e.g. Odoo Enterprise Subscription"/></h1>
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 7ae0885700d..1b640a30d62 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -434,9 +434,6 @@ class PurchaseOrderLine(models.Model):
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
product_uom = fields.Many2one('uom.uom', string='Product Unit of Measure', required=True)
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
- product_image = fields.Binary(
- 'Product Image', related="product_id.image", readonly=False,
- help="Non-stored related field to allow portal user to see the image of the product he has ordered")
product_type = fields.Selection(related='product_id.type', readonly=True)
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'))
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 9144791f4e1..ab645e7b476 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1114,9 +1114,6 @@ class SaleOrderLine(models.Model):
# because the result might be different then.
product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string='Product attribute values that do not create variants')
- # Non-stored related field to allow portal user to see the image of the product he has ordered
- product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=False)
-
qty_delivered_method = fields.Selection([
('manual', 'Manual'),
('analytic', 'Analytic From Expenses')
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 4f0f84cebf6..5e4956a7319 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -1220,10 +1220,10 @@ class WebsiteSale(ProductConfiguratorController):
"""
kw.pop('pricelist_id')
res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
-
- if request.env.ref('website_sale.shop_product_carousel', raise_if_not_found=False): # IF for compatibility 12.0
- res.update(carousel=request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel', values={
+ carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel',
+ values={
'product': request.env['product.template'].browse(res['product_template_id']),
'product_variant': request.env['product.product'].browse(res['product_id']),
- }))
+ })
+ res['carousel'] = carousel_view
return res
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml
index 5cecc496363..71f7a2d7db7 100644
--- a/addons/website_sale/data/mail_template_data.xml
+++ b/addons/website_sale/data/mail_template_data.xml
@@ -43,7 +43,7 @@
<table width="100%">
<tr>
<td style="padding: 10px; width:150px;">
- <img src="/web/image/product.product/${line.product_id.id}/image" height="100px" width="100px" alt="Product image"></img>
+ <img src="/web/image/product.product/${line.product_id.id}/image_medium" height="100px" width="100px" alt="Product image"></img>
</td>
<td>
<strong>${line.product_id.display_name}</strong><br/>${line.name}
diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py
index ed453ce17ac..e46b59c947a 100644
--- a/addons/website_sale/models/__init__.py
+++ b/addons/website_sale/models/__init__.py
@@ -5,6 +5,7 @@ from . import account_invoice
from . import crm_team
from . import ir_http
from . import mail_compose_message
+from . import product_image
from . import product
from . import res_country
from . import res_partner
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index f82572c0afd..5670bbcb561 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from odoo import api, fields, models, tools, _
from odoo.addons import decimal_precision as dp
from odoo.tools.translate import html_translate
@@ -124,7 +125,8 @@ class ProductTemplate(models.Model):
public_categ_ids = fields.Many2many('product.public.category', string='Website Product Category',
help="The product will be available in each mentioned e-commerce category. Go to"
"Shop > Customize and enable 'E-commerce categories' to view all e-commerce categories.")
- product_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images')
+
+ product_template_image_ids = fields.One2many('product.image', 'product_tmpl_id', string="Extra Product Images", copy=True)
# website_price deprecated, directly use _get_combination_info instead
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
@@ -346,12 +348,28 @@ class ProductTemplate(models.Model):
domain = super(ProductTemplate, self)._rating_domain()
return expression.AND([domain, [('website_published', '=', True)]])
+ @api.multi
+ def _get_images(self):
+ """Return a list of records implementing `image.mixin` to
+ display on the carousel on the website for this template.
+
+ This returns a list and not a recordset because the records might be
+ from different models (template and image).
+
+ It contains in this order: the main image of the template and the
+ Template Extra Images.
+ """
+ self.ensure_one()
+ return [self] + list(self.product_template_image_ids)
+
class Product(models.Model):
_inherit = "product.product"
website_id = fields.Many2one(related='product_tmpl_id.website_id', readonly=False)
+ product_variant_image_ids = fields.One2many('product.image', 'product_variant_id', string="Extra Variant Images")
+
# website_price deprecated, directly use _get_combination_info instead
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
# website_public_price deprecated, directly use _get_combination_info instead
@@ -371,11 +389,27 @@ class Product(models.Model):
self.ensure_one()
return self.product_tmpl_id.website_publish_button()
+ @api.multi
+ def _get_images(self):
+ """Return a list of records implementing `image.mixin` to
+ display on the carousel on the website for this variant.
-class ProductImage(models.Model):
- _name = 'product.image'
- _description = 'Product Image'
+ This returns a list and not a recordset because the records might be
+ from different models (template, variant and image).
- name = fields.Char('Name')
- image = fields.Binary('Image', attachment=True)
- product_tmpl_id = fields.Many2one('product.template', 'Related Product', copy=True)
+ It contains in this order: the main image of the variant (if set), the
+ Variant Extra Images, and the Template Extra Images.
+ """
+ self.ensure_one()
+ variant_images = list(self.product_variant_image_ids)
+ if self.image_raw_original:
+ # if the main variant image is set, display it first
+ variant_images = [self] + variant_images
+ else:
+ # If the main variant image is empty, it will fallback to template
+ # image, in this case insert it after the other variant images, so
+ # that all variant images are first and all template images last.
+ variant_images = variant_images + [self]
+ # [1:] to remove the main image from the template, we only display
+ # the template extra images here
+ return variant_images + self.product_tmpl_id._get_images()[1:]
diff --git a/addons/website_sale/models/product_image.py b/addons/website_sale/models/product_image.py
new file mode 100644
index 00000000000..b447572e15f
--- /dev/null
+++ b/addons/website_sale/models/product_image.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields
+
+
+class ProductImage(models.Model):
+ _name = 'product.image'
+ _description = "Product Image"
+ _inherit = ['image.mixin']
+ _order = 'sequence, id'
+
+ name = fields.Char("Name", required=True)
+ sequence = fields.Integer(default=10, index=True)
+
+ image_original = fields.Binary(required=True)
+
+ product_tmpl_id = fields.Many2one('product.template', "Product Template", index=True, ondelete='cascade')
+ product_variant_id = fields.Many2one('product.product', "Product Variant", index=True, ondelete='cascade')
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index ed03f81c5de..94e9c54a312 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -209,6 +209,24 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM
}
return ProductConfiguratorMixin.getSelectedVariantValues.apply(this, arguments);
},
+ /**
+ * Write the properties of the form elements in the DOM to prevent the
+ * current selection from being lost when activating the web editor.
+ *
+ * @override
+ */
+ onChangeVariant: function (ev) {
+ var $component = $(ev.currentTarget).closest('.js_product');
+ $component.find('input').each(function () {
+ var $el = $(this);
+ $el.attr('checked', $el.is(':checked'));
+ });
+ $component.find('select option').each(function () {
+ var $el = $(this);
+ $el.attr('selected', $el.is(':selected'));
+ });
+ return ProductConfiguratorMixin.onChangeVariant.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Private
@@ -345,21 +363,12 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM
// Do not activate image zoom for mobile devices, since it might prevent users from scrolling the page
if (!config.device.isMobile) {
var autoZoom = $('.ecom-zoomable').data('ecom-zoom-auto') || false,
- factorZoom = parseFloat($('.ecom-zoomable').data('ecom-zoom-factor')) || 1.5,
attach = '#o-carousel-product';
_.each($('.ecom-zoomable img[data-zoom]'), function (el) {
onImageLoaded(el, function () {
var $img = $(el);
- if (!_.str.endsWith(el.src, el.dataset.zoomImage) || // if zoom-img != img
- el.naturalWidth >= $(attach).width() * factorZoom || el.naturalHeight >= $(attach).height() * factorZoom) {
- $img.zoomOdoo({event: autoZoom ? 'mouseenter' : 'click', attach: attach});
- $img.attr('data-zoom', 1); // add cursor (if previously removed)
- } else {
- $img.removeAttr('data-zoom'); // remove cursor
- // remove zooming but keep the attribute because
- // it can potentially be set back
- $img.attr('data-zoom-image', '');
- }
+ $img.zoomOdoo({event: autoZoom ? 'mouseenter' : 'click', attach: attach});
+ $img.attr('data-zoom', 1);
});
});
}
diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js b/addons/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js
index f23d8927d6d..0a3b3b19f0c 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js
@@ -5,12 +5,12 @@ odoo.define("website_sale.tour_shop_custom_attribute_value", function (require)
var base = require("web_editor.base");
tour.register("shop_custom_attribute_value", {
- url: "/shop",
+ url: "/shop?search=Customizable Desk",
test: true,
wait_for: base.ready()
}, [{
- trigger: 'img[src*="/product.product/10"]',
- run: 'click'
+ content: "click on Customizable Desk",
+ trigger: '.oe_product_cart a:contains("Customizable Desk")',
}, {
trigger: 'li.js_attribute_value span:contains(Custom)',
extra_trigger: 'li.js_attribute_value',
diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss
index 6a5d755ad64..a8ec3aea1bf 100644
--- a/addons/website_sale/static/src/scss/website_sale.scss
+++ b/addons/website_sale/static/src/scss/website_sale.scss
@@ -433,14 +433,6 @@ div#payment_method {
.oe_website_sale .row .row .col-lg-12 {
float: none;
}
-/* product detail */
-
-.product_detail_img {
- margin-left: auto;
- margin-right: auto;
- display: block;
- max-height: 500px;
-}
.oe_website_sale input.js_quantity {
min-width: 48px;
@@ -615,46 +607,36 @@ a.no-decoration {
opacity: 0.2;
}
- .carousel-inner div.item, .carousel-control-prev, .carousel-control-next {
+ .carousel-outer {
height: 400px;
- }
- .carousel-inner div.item img {
- max-height: 100%;
+ max-height: 90vh;
}
- .carousel-control-prev, carousel-control-next {
- background-image: none !important;
+ .carousel-control-prev, .carousel-control-next {
+ opacity: 0.5;
+ cursor: pointer;
+ &:focus {
+ opacity: 0.65;
+ }
+ &:hover {
+ opacity: 0.8;
+ }
+ > span {
+ background: rgba(0, 0, 0, 0.8);
+ }
}
.carousel-indicators {
- background-color: transparent;
- margin: 10px 0 0;
- overflow-y: hidden;
- position: static;
- text-align: left;
- white-space: nowrap;
- width: 100%;
- height: 100px !important;
- text-align: center;
- list-style-position: inside;
-
li {
- background-color: transparent !important;
- border-radius: 0;
- display: inline-block;
- margin: 0 !important;
- width: 54px !important;
- height: 54px !important;
- border: 0 !important;
- box-shadow: none !important;
-
- img {
- display: block;
- opacity: 0.5;
- }
+ width: 64px;
+ height: 64px;
+ text-indent: unset;
+ border: 1px solid gray('600');
+ opacity: 0.5;
- &.active img {
+ &.active {
opacity: 1;
+ border: 1px solid theme-color('primary');
}
}
}
diff --git a/addons/website_sale/static/src/scss/website_sale_backend.scss b/addons/website_sale/static/src/scss/website_sale_backend.scss
index 9220879c743..29ddb944fef 100644
--- a/addons/website_sale/static/src/scss/website_sale_backend.scss
+++ b/addons/website_sale/static/src/scss/website_sale_backend.scss
@@ -1,12 +1,9 @@
-.o_website_sale_image_list .o_kanban_view.o_kanban_ungrouped.o_attachments_previews {
+.o_website_sale_image_list .o_kanban_view.o_kanban_ungrouped {
width: auto;
- .o_kanban_record.o_attachment.o_website_sale_image_card {
- border: none;
- background: none;
- margin: 0 0 $grid-gutter-width*0.2;
- padding: $o-mail-attachment-margin;
+ .o_kanban_record{
flex: 0 1 50%;
+ position: relative;
@include media-breakpoint-up(md) {
flex: 0 0 percentage(1/3);
@@ -19,44 +16,25 @@
@include media-breakpoint-up(xl) {
flex: 0 0 percentage(1/6);
}
-
- .o_image_box {
- padding-top: 50%;
-
- @include media-breakpoint-up(md) {
- padding-top: 75%;
- }
- }
-
- .o_image_overlay {
- bottom: 16%;
- background: rgba(gray('900'), 0.2);
- }
-
- .o_website_sale_image_card_title {
- @include o-text-overflow(block);
- padding:6px;
+ // make the image square and in the center
+ .o_squared_image {
position: relative;
- background: gray('200');
- color:$headings-color;
- }
-
- .o_website_sale_image_card_remove, .o_website_sale_image_card_size {
- @include o-position-absolute(0, 0);
- border-radius: 0 2px;
+ overflow: hidden;
+ padding-bottom: 100%;
+ > img {
+ position: absolute;
+ margin: auto;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ }
}
- .o_website_sale_image_card_size {
+ .o_product_image_size {
+ position: absolute;
+ top: 0;
left: 0;
- right: auto;
- border-radius: 0 0 2px;
- font-weight: normal;
- font-size: $font-size-sm - 1;
-
- &.label-normal {
- background-color: gray('200');
- color: $body-color;
- }
}
}
}
diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml
index 98235ff9831..1afe4f9b9fc 100644
--- a/addons/website_sale/views/product_views.xml
+++ b/addons/website_sale/views/product_views.xml
@@ -102,47 +102,27 @@
<field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
</group>
</group>
- <group name="extra_images" string="Extra Images">
- <field name="product_image_ids" class="o_website_sale_image_list" context="{'default_name': name, 'default_product_tmpl_id': active_id}" nolabel="1">
- <kanban string="Product Images" class="o_attachments_previews">
- <field name="id"/>
- <field name="name"/>
- <field name="image"/>
- <templates>
- <t t-name="kanban-box">
- <div class="o_attachment oe_kanban_global_click o_website_sale_image_card">
- <div class="o_attachment_wrap">
- <div class="o_image_box">
- <div class="o_attachment_image" t-attf-style="background-image:url({{kanban_image('product.image', 'image', record.id.raw_value)}});"/>
- <div t-attf-class="o_image_overlay" t-att-title="record.name.value"/>
- <div class="o_website_sale_image_card_title" t-att-title="record.name.value">
- <field name="name"/>
- </div>
- </div>
-
- <t t-set="size" t-value="[record.image.value][0]"/>
- <t t-set="size_status" t-value="'normal'" t-if="size.indexOf('Bytes') !== -1 or ( size.indexOf('Kb') !== -1 and size[2]=='.')"/>
- <t t-set="size_status" t-value="'warning'" t-elif="( size.indexOf('Kb') !== -1 and size[3]=='.')"/>
- <t t-set="size_status" t-value="'danger'" t-else=""/>
- <t t-set="message" t-value="'Acceptable file size'" t-if="size_status == 'normal'"></t>
- <t t-set="message" t-value="'Huge file size. The image should be optimized/reduced.'" t-elif="size_status == 'warning'"></t>
- <t t-set="message" t-value="'Optimization required! Reduce the image size or increase your compression settings.'" t-else=""></t>
-
- <span t-attf-class="o_website_sale_image_card_size badge badge-#{size_status}" t-att-title="message">
- <t t-esc="record.image.value"/>
- </span>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
+ <group name="product_template_images" string="Extra Product Images">
+ <field name="product_template_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" options="{'create_text':'Add an Image'}" nolabel="1"/>
</group>
</page>
</xpath>
</field>
</record>
+ <record id="product_product_view_form_easy_inherit_website_sale" model="ir.ui.view">
+ <field name="name">product.product.view.form.easy.inherit.website_sale</field>
+ <field name="model">product.product</field>
+ <field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
+ <field name="arch" type="xml">
+ <sheet position="inside">
+ <group name="product_variant_images" string="Extra Variant Images">
+ <field name="product_variant_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" options="{'create_text':'Add an Image'}" nolabel="1"/>
+ </group>
+ </sheet>
+ </field>
+ </record>
+
<!-- Product Public Categories -->
<record id="product_public_category_form_view" model="ir.ui.view">
<field name="name">product.public.category.form</field>
@@ -216,25 +196,68 @@
</field>
</record>
- <record model="ir.ui.view" id="view_product_image_form">
- <field name="name">product.image.form</field>
+ <!-- This view should only be used from the product o2m because the required field product_tmpl_id has to be automatically set. -->
+ <record id="view_product_image_form" model="ir.ui.view">
+ <field name="name">product.image.view.form</field>
<field name="model">product.image</field>
<field name="arch" type="xml">
<form string="Product Images">
<div class="row o_website_sale_image_modal">
<div class="col-md-6 col-xl-5">
- <div class="oe_title">
- <label class="small text-muted" for="name" string="Image Name"/>
- <h2><field name="name" placeholder="Image Name" required="True"/></h2>
- <field name="product_tmpl_id" invisible="1"/>
- </div>
+ <label for="name" string="Image Name"/>
+ <h2><field name="name" placeholder="Image Name"/></h2>
+ <!-- Unfortunately for now we can't drag and drop kanban o2m, so we have to let the user input the sequence manually. -->
+ <label for="sequence" string="Sequence"/><br/>
+ <field name="sequence"/>
</div>
<div class="col-md-6 col-xl-7 text-center o_website_sale_image_modal_container">
- <field name="image" widget="image"/>
+ <field name="image_original" widget="image"/>
</div>
</div>
</form>
</field>
</record>
+ <record id="product_image_view_kanban" model="ir.ui.view">
+ <field name="name">product.image.view.kanban</field>
+ <field name="model">product.image</field>
+ <field name="arch" type="xml">
+ <kanban string="Product Images" default_order="sequence">
+ <field name="id"/>
+ <field name="name"/>
+ <field name="image_original"/>
+ <field name="sequence"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="card oe_kanban_global_click p-0">
+ <div class="o_squared_image">
+ <img class="card-img-top" t-att-src="kanban_image('product.image', 'image_original', record.id.value)" t-att-alt="record.name.value"/>
+ </div>
+ <div class="card-body p-0">
+ <h4 class="card-title p-2 m-0 bg-200">
+ <small><field name="name"/></small>
+ </h4>
+ </div>
+ <!-- below 100 Kb: good -->
+ <t t-if="record.image_original.raw_value.length &lt; 100*1000">
+ <t t-set="size_status" t-value="'badge-success'"/>
+ <t t-set="message">Acceptable file size</t>
+ </t>
+ <!-- below 1000 Kb: decent -->
+ <t t-elif="record.image_original.raw_value.length &lt; 1000*1000">
+ <t t-set="size_status" t-value="'badge-warning'" />
+ <t t-set="message">Huge file size. The image should be optimized/reduced.</t>
+ </t>
+ <!-- above 1000 Kb: bad -->
+ <t t-else="1">
+ <t t-set="size_status" t-value="'badge-danger'"/>
+ <t t-set="message">Optimization required! Reduce the image size or increase your compression settings.</t>
+ </t>
+ <span t-attf-class="badge #{size_status} o_product_image_size" t-esc="record.image_original.value" t-att-title="message"/>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
</odoo>
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 4ab035c4d46..58064782b17 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -99,11 +99,10 @@
<!-- Product item used by /shop and /shop/cart -->
<template id="products_item" name="Products item">
- <t t-set="product_href" t-value="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" />
+ <t t-set="product_href" t-value="request.httprequest.url_root + keep('shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" />
<t t-set="first_possible_combination" t-value="product._get_first_possible_combination()"/>
<t t-set="combination_info" t-value="product._get_combination_info(first_possible_combination, add_qty=add_qty or 1, pricelist=pricelist)"/>
- <t t-set="product_variant" t-value="product.env['product.product'].browse(combination_info['product_id'])"/>
<form action="/shop/cart/update" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
@@ -113,8 +112,7 @@
</div>
<div class="oe_product_image">
<a itemprop="url" t-att-href="product_href">
- <span t-if="product_variant" itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.product/{{product_variant.id}}/image" t-field="product_variant.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
- <span t-else="" itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
+ <span t-field="product.image" t-options="{'widget': 'image', 'preview_image': 'image' if product_image_big else 'image_large'}"/>
</a>
</div>
<t t-if="show_publish">
@@ -457,45 +455,10 @@
</div>
</div>
<div class="row">
- <div class="col-md-6">
- <t t-set="variant_img" t-value="product_variant and product_variant.image_variant"/>
- <t t-set="image_ids" t-value="product.product_image_ids"/>
- <div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
- <div class="carousel-outer">
- <div class="carousel-inner">
- <div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product_variant.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product_variant['__last_update']) or '')}"/>
- <div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
- <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
- <div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
- </t>
- </div>
-
- <t t-if="len(image_ids) or variant_img">
- <a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev" >
- <span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/>
- </a>
- <a class="carousel-control-next" href="#o-carousel-product" role="button" data-slide="next">
- <span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/>
- </a>
- </t>
- </div>
-
- <ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
- <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
- <img class="img img-fluid js_variant_img_small" t-attf-src="/website/image/product.product/{{product_variant.id}}/image/90x90" t-att-alt="product.name"/>
- </li>
- <li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else '0'" t-att-class="'' if variant_img else 'active'">
- <img class="img img-fluid" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
- </li>
- <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
- <li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
- <img class="img img-fluid" t-attf-src="/website/image/product.image/{{pimg.id}}/image/90x90" t-att-alt="pimg.name"/>
- </li>
- </t>
- </ol>
- </div>
+ <div class="col-md-6 col-xl-8">
+ <t t-call="website_sale.shop_product_carousel"/>
</div>
- <div class="col-md-6 col-xl-4 offset-xl-2" id="product_details">
+ <div class="col-md-6 col-xl-4" id="product_details">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
<span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/>
<form t-if="product._is_add_to_cart_possible()" action="/shop/cart/update" method="POST">
@@ -1707,41 +1670,32 @@
</template>
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
- <t t-set="variant_img" t-value="product_variant and product_variant.image_variant"/>
- <t t-set="image_ids" t-value="product.product_image_ids"/>
+ <t t-set="product_images" t-value="product_variant._get_images() if product_variant else product._get_images()"/>
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
- <div class="carousel-outer">
- <div class="carousel-inner">
- <div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product_variant.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product_variant['__last_update']) or '')}"/>
- <div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
- <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
- <div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
+ <div class="carousel-outer position-relative">
+ <div class="carousel-inner h-100">
+ <t t-foreach="product_images" t-as="product_image">
+ <div t-attf-class="carousel-item h-100#{' active' if product_image_first else ''}">
+ <div t-field="product_image.image" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_be_zoomed and "image_original"}'/>
+ </div>
</t>
</div>
-
- <t t-if="len(image_ids) or variant_img">
- <a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev" >
- <span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/>
+ <t t-if="len(product_images) > 1">
+ <a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev">
+ <span class="fa fa-chevron-left p-2" role="img" aria-label="Previous" title="Previous"/>
</a>
<a class="carousel-control-next" href="#o-carousel-product" role="button" data-slide="next">
- <span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/>
+ <span class="fa fa-chevron-right p-2" role="img" aria-label="Next" title="Next"/>
</a>
</t>
</div>
-
- <ol t-if="len(image_ids) or variant_img" class="carousel-indicators">
- <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
- <img class="img img-fluid js_variant_img_small" t-attf-src="/website/image/product.product/{{product_variant.id}}/image/90x90" t-att-alt="product.name"/>
- </li>
- <li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else '0'" t-att-class="'' if variant_img else 'active'">
- <img class="img img-fluid" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
- </li>
- <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
- <li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
- <img class="img img-fluid" t-attf-src="/website/image/product.image/{{pimg.id}}/image/90x90" t-att-alt="pimg.name"/>
- </li>
- </t>
- </ol>
+ <div t-ignore="True" class="d-none d-md-block text-center">
+ <ol t-if="len(product_images) > 1" class="carousel-indicators d-inline-block position-static mx-auto my-0 p-1 text-left">
+ <t t-foreach="product_images" t-as="product_image"><li t-attf-class="d-inline-block m-1 align-top {{'active' if product_image_first else ''}}" data-target="#o-carousel-product" t-att-data-slide-to="str(product_image_index)">
+ <div t-field="product_image.image_small" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "alt-field": "name"}'/>
+ </li></t>
+ </ol>
+ </div>
</div>
</template>
diff --git a/addons/website_sale_comparison/views/website_sale_comparison_template.xml b/addons/website_sale_comparison/views/website_sale_comparison_template.xml
index d6c55944800..ecf16c94bcb 100644
--- a/addons/website_sale_comparison/views/website_sale_comparison_template.xml
+++ b/addons/website_sale_comparison/views/website_sale_comparison_template.xml
@@ -91,7 +91,7 @@
<strong class='float-right'>x</strong>
</a>
<a t-att-href="'/shop/product/%s' % slug(product.product_tmpl_id)">
- <img t-attf-src="/web/image/product.product/#{product.id}/image/200x150" class="img img-fluid" style="margin:auto;" alt="Product image"/>
+ <img t-attf-src="/web/image/product.product/#{product.id}/image_large" class="img img-fluid" style="margin:auto;" alt="Product image"/>
</a>
<div class='product_summary'>
<a class="o_product_comparison_table" t-attf-href="/shop/product/#{ slug(product.product_tmpl_id) }">
diff --git a/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml b/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
index 55ec4bd8f89..0bd94c653c4 100644
--- a/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
+++ b/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
@@ -58,7 +58,7 @@
<tr t-att-data-wish-id='wish.id' t-att-data-product-id='wish.product_id.id'>
<td class='td-img'>
<a t-att-href="'/shop/product/%s' % slug(wish.product_id.product_tmpl_id)">
- <img t-attf-src="/web/image/product.product/#{wish.product_id.id}/image/100x100" class="img img-fluid" style="margin:auto;" alt="Product image"/>
+ <img t-attf-src="/web/image/product.product/#{wish.product_id.id}/image_medium" class="img img-fluid" style="margin:auto;" alt="Product image"/>
</a>
</td>
<td class='text-left'>