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"}'/>
-
|
- |
${line.product_id.display_name} ${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; + width: 64px; + height: 64px; + text-indent: unset; + border: 1px solid gray('600'); + opacity: 0.5; - img { - display: block; - 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,45 +16,26 @@ @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; - } - - .o_website_sale_image_card_size { - 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; + overflow: hidden; + padding-bottom: 100%; + > img { + position: absolute; + margin: auto; + top: 0; + left: 0; + bottom: 0; + right: 0; } } + + .o_product_image_size { + position: absolute; + top: 0; + left: 0; + } } } 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 @@
+
+
+
+
+
+
+
+
+ |