[IMP] website_sale: add alternative product image displays
This commit adds a new display type for a product page, namely the grid mode. The web designer will now be able to choose between carousel (previous mode) and grid. The web designer may also choose how much space the images take on the page between 0%, 50% 66% and 100%. TaskId-2833507 Part-of: odoo/odoo#92447
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
'views/website_sale_visitor_views.xml',
|
||||
'views/base_unit_view.xml',
|
||||
'views/product_product_add.xml',
|
||||
'views/website_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
|
||||
@@ -1207,12 +1207,21 @@ class WebsiteSale(http.Controller):
|
||||
raise NotFound()
|
||||
|
||||
current_website = request.env['website'].get_current_website()
|
||||
if "ppg" in options:
|
||||
current_website.shop_ppg = options["ppg"] or 1
|
||||
if "ppr" in options:
|
||||
current_website.shop_ppr = options["ppr"]
|
||||
if "default_sort" in options:
|
||||
current_website.shop_default_sort = options["default_sort"]
|
||||
# Restrict options we can write to.
|
||||
writable_fields = {
|
||||
'shop_ppg', 'shop_ppr', 'shop_default_sort',
|
||||
'product_page_image_layout', 'product_page_image_width',
|
||||
'product_page_grid_columns', 'product_page_image_spacing'
|
||||
}
|
||||
# Default ppg to 1.
|
||||
if 'ppg' in options and not options['ppg']:
|
||||
options['ppg'] = 1
|
||||
if 'product_page_grid_columns' in options:
|
||||
options['product_page_grid_columns'] = int(options['product_page_grid_columns'])
|
||||
|
||||
write_vals = {k: v for k, v in options.items() if k in writable_fields}
|
||||
if write_vals:
|
||||
current_website.write(write_vals)
|
||||
|
||||
def order_lines_2_google_api(self, order_lines):
|
||||
""" Transforms a list of order lines into a dict for google analytics """
|
||||
|
||||
@@ -18,11 +18,13 @@ class WebsiteSaleVariantController(VariantController):
|
||||
if request.website.google_analytics_key:
|
||||
combination['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination)
|
||||
|
||||
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_carousel', values={
|
||||
'product': request.env['product.template'].browse(combination['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(combination['product_id']),
|
||||
})
|
||||
combination['carousel'] = carousel_view
|
||||
if request.website.product_page_image_width != 'none':
|
||||
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_images', values={
|
||||
'product': request.env['product.template'].browse(combination['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(combination['product_id']),
|
||||
'website': request.env['website'].get_current_website(),
|
||||
})
|
||||
combination['carousel'] = carousel_view
|
||||
return combination
|
||||
|
||||
@http.route(auth="public")
|
||||
|
||||
@@ -80,6 +80,27 @@ class Website(models.Model):
|
||||
],
|
||||
default='optional')
|
||||
|
||||
product_page_image_layout = fields.Selection([
|
||||
('carousel', 'Carousel'),
|
||||
('grid', 'Grid'),
|
||||
], default='carousel', required=True,
|
||||
)
|
||||
product_page_grid_columns = fields.Integer(default=2)
|
||||
product_page_image_width = fields.Selection([
|
||||
('none', 'Hidden'),
|
||||
('50_pc', '50 %'),
|
||||
('66_pc', '66 %'),
|
||||
('100_pc', '100 %'),
|
||||
], default='50_pc', required=True,
|
||||
)
|
||||
product_page_image_spacing = fields.Selection([
|
||||
('none', 'None'),
|
||||
('small', 'Small'),
|
||||
('medium', 'Medium'),
|
||||
('big', 'Big'),
|
||||
], default='small', required=True,
|
||||
)
|
||||
|
||||
@api.depends('all_pricelist_ids')
|
||||
def _compute_pricelist_ids(self):
|
||||
for website in self:
|
||||
@@ -462,6 +483,33 @@ class Website(models.Model):
|
||||
result.append(self.env['product.template']._search_get_detail(self, order, options))
|
||||
return result
|
||||
|
||||
def _get_product_page_proportions(self):
|
||||
"""
|
||||
Returns the number of columns (css) that both the images and the product details should take.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'none': (0, 12),
|
||||
'50_pc': (6, 6),
|
||||
'66_pc': (8, 4),
|
||||
'100_pc': (12, 12),
|
||||
}.get(self.product_page_image_width)
|
||||
|
||||
def _get_product_page_grid_image_classes(self):
|
||||
spacing_map = {
|
||||
'none': 'p-0',
|
||||
'small': 'p-2',
|
||||
'medium': 'p-3',
|
||||
'big': 'p-4',
|
||||
}
|
||||
columns_map = {
|
||||
1: 'col-12',
|
||||
2: 'col-6',
|
||||
3: 'col-4',
|
||||
}
|
||||
return spacing_map.get(self.product_page_image_spacing) + ' ' +\
|
||||
columns_map.get(self.product_page_grid_columns)
|
||||
|
||||
class WebsiteSaleExtraField(models.Model):
|
||||
_name = 'website.sale.extra.field'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3594 0V8H0.359375V0H23.3594ZM18.3594 1V3.5H15.3594V4.5H18.3594V7L21.3594 4L18.3594 1ZM5.35938 1L2.35938 4L5.35938 7V4.5H8.35938V3.5H5.35938V1Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="23" height="8" viewBox="0 0 23 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 0H12V8H21V0Z" fill="#CDCDCD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0V8H11V0H2Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.3594 0H15.3594V8H21.3594V0Z" fill="#CDCDCD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.35938 0V8H13.3594V0H2.35938Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 0V8H21V0H2ZM14.36 5.56C14.45 5.65 14.5 5.75 14.5 5.87C14.5 5.99 14.45 6.1 14.36 6.18L13.69 6.8C13.6 6.89 13.49 6.93 13.35 6.93C13.21 6.93 13.11 6.89 13.01 6.8L11.56 5.46L10.11 6.8C10.02 6.89 9.91 6.93 9.77 6.93C9.63 6.93 9.53 6.89 9.43 6.8L8.76 6.18C8.67 6.09 8.62 5.99 8.62 5.87C8.62 5.75 8.67 5.64 8.76 5.56L10.21 4.22L8.76 2.88C8.67 2.79 8.62 2.69 8.62 2.57C8.62 2.45 8.67 2.34 8.76 2.26L9.43 1.64C9.52 1.55 9.63 1.51 9.77 1.51C9.91 1.51 10.01 1.55 10.11 1.64L11.56 2.98L13.01 1.64C13.1 1.55 13.21 1.51 13.35 1.51C13.49 1.51 13.59 1.55 13.69 1.64L14.36 2.26C14.45 2.35 14.5 2.45 14.5 2.57C14.5 2.69 14.45 2.8 14.36 2.88L12.91 4.22L14.36 5.56Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@@ -234,7 +234,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
|
||||
return this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params: {
|
||||
'ppg': ppg,
|
||||
'shop_ppg': ppg,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -246,7 +246,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params: {
|
||||
'ppr': this.ppr,
|
||||
'shop_ppr': this.ppr,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -258,7 +258,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params: {
|
||||
'default_sort': this.default_sort,
|
||||
'shop_default_sort': this.default_sort,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -643,6 +643,96 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
|
||||
}
|
||||
});
|
||||
|
||||
options.registry.WebsiteSaleProductPage = options.Class.extend({
|
||||
|
||||
_updateWebsiteConfig(params) {
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params,
|
||||
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setImageWidth(previewMode, widgetValue, params) {
|
||||
this._updateWebsiteConfig({
|
||||
product_page_image_width: widgetValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setImageLayout(previewMode, widgetValue, params) {
|
||||
this._updateWebsiteConfig({
|
||||
product_page_image_layout: widgetValue,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeWidgetState(methodName, params) {
|
||||
switch (methodName) {
|
||||
case 'setImageWidth':
|
||||
return this.$target.data('image_width');
|
||||
case 'setImageLayout':
|
||||
return this.$target.data('image_layout');
|
||||
}
|
||||
return this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
||||
options.registry.WebsiteSaleProductPageGrid = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setSpacing(previewMode, widgetValue, params) {
|
||||
const spacing = {
|
||||
0: 'none',
|
||||
1: 'small',
|
||||
2: 'medium',
|
||||
3: 'big',
|
||||
}[widgetValue];
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params: {
|
||||
'product_page_image_spacing': spacing,
|
||||
},
|
||||
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
|
||||
this.$target.data('image_spacing', spacing);
|
||||
},
|
||||
|
||||
setColumns(previewMode, widgetValue, params) {
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
params: {
|
||||
'product_page_grid_columns': widgetValue,
|
||||
},
|
||||
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
|
||||
this.$target.data('grid_columns', widgetValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeWidgetState(methodName, params) {
|
||||
switch (methodName) {
|
||||
case 'setSpacing':
|
||||
return {
|
||||
'none': 0,
|
||||
'small': 1,
|
||||
'medium': 2,
|
||||
'big': 3,
|
||||
}[this.$target.data('image_spacing')];
|
||||
case 'setColumns':
|
||||
return this.$target.data('grid_columns');
|
||||
}
|
||||
return this._super(methodName, params);
|
||||
}
|
||||
});
|
||||
|
||||
options.registry.WebsiteSaleProductAttribute = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -36,7 +36,10 @@ const cartHandlerMixin = {
|
||||
}).then(async data => {
|
||||
sessionStorage.setItem('website_sale_cart_quantity', data.cart_quantity);
|
||||
if (data.cart_quantity && (data.cart_quantity !== parseInt($(".my_cart_quantity").text()))) {
|
||||
await animateClone($('header .o_wsale_my_cart').first(), this.$itemImgContainer, 25, 40);
|
||||
// No animation if the product's page images are hidden
|
||||
if ($('div[data-image_width]').data('image_width') !== 'none') {
|
||||
await animateClone($('header .o_wsale_my_cart').first(), this.$itemImgContainer, 25, 40);
|
||||
}
|
||||
updateCartNavBar(data);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -185,6 +185,18 @@
|
||||
data-reload="/"/>
|
||||
</div>
|
||||
<!-- Product image -->
|
||||
<div data-js="WebsiteSaleProductPage" data-selector="#product_detail_main" data-no-check="true">
|
||||
<we-button-group string="Images Width" data-no-preview="true" data-reload="/">
|
||||
<we-button data-set-image-width="none" data-img="/website_sale/static/src/img/snippet_options/image-width-none.svg" title="None"/>
|
||||
<we-button data-set-image-width="50_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-50.svg" title="50 percent"/>
|
||||
<we-button data-set-image-width="66_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-66.svg" title="66 percent"/>
|
||||
<we-button data-set-image-width="100_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-100.svg" title="100 percent"/>
|
||||
</we-button-group>
|
||||
<we-select string="Layout" data-no-preview="true" data-reload="/">
|
||||
<we-button data-set-image-layout="carousel">Carousel</we-button>
|
||||
<we-button data-set-image-layout="grid">Grid</we-button>
|
||||
</we-select>
|
||||
</div>
|
||||
<div data-selector="#product_detail #o-carousel-product" data-no-check="true">
|
||||
<we-select string="Image Zoom" data-no-preview="true" data-reload="/">
|
||||
<we-button data-customize-website-views="">None</we-button>
|
||||
@@ -196,6 +208,14 @@
|
||||
<we-button class="fa fa-fw fa-long-arrow-down" title="Bottom" data-customize-website-views="website_sale.carousel_product_indicators_bottom"/>
|
||||
</we-button-group>
|
||||
</div>
|
||||
<div data-js="WebsiteSaleProductPageGrid" data-selector="#product_detail #o-grid-product" data-no-check="true">
|
||||
<we-range string="Image Spacing" data-no-preview="true" data-reload="/" data-max="3" data-step="1" data-set-spacing=""/>
|
||||
<we-select string="Columns" data-no-preview="true" data-reload="/">
|
||||
<we-button data-set-columns="1">1</we-button>
|
||||
<we-button data-set-columns="2">2</we-button>
|
||||
<we-button data-set-columns="3">3</we-button>
|
||||
</we-select>
|
||||
</div>
|
||||
<div data-js="WebsiteSaleProductAttribute" data-selector="#product_detail .o_wsale_product_attribute" data-no-check="true">
|
||||
<we-select string="Display Type" data-no-preview="true">
|
||||
<we-button data-set-display-type="radio">Radio</we-button>
|
||||
|
||||
@@ -624,11 +624,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mt-lg-4">
|
||||
<t t-call="website_sale.shop_product_carousel"/>
|
||||
<div class="row" id="product_detail_main" data-name="Product Page"
|
||||
t-att-data-image_width="website.product_page_image_width"
|
||||
t-att-data-image_layout="website.product_page_image_layout">
|
||||
<t t-set="image_cols" t-value="website._get_product_page_proportions()"/>
|
||||
<div t-attf-class="col-lg-#{image_cols[0]} mt-lg-4 o_wsale_product_images" t-if="website.product_page_image_width != 'none'">
|
||||
<t t-call="website_sale.shop_product_images"/>
|
||||
</div>
|
||||
<div class="col-lg-6 mt-md-4" id="product_details">
|
||||
<div t-attf-class="col-lg-#{image_cols[1]} mt-md-4" id="product_details">
|
||||
<h1 itemprop="name" t-field="product.name">Product Name</h1>
|
||||
<span itemprop="url" style="display:none;" t-esc="product.website_url"/>
|
||||
<t t-if="is_view_active('website_sale.product_comment')">
|
||||
@@ -1975,27 +1978,38 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
|
||||
<!-- Product page images -->
|
||||
<template id="website_sale.shop_product_images" name="Shop Product Images">
|
||||
<t t-set="product_images" t-value="product_variant._get_images() if product_variant else product._get_images()"/>
|
||||
<t t-set="product_carousel_block_name">Product Image</t>
|
||||
<t t-call="website_sale.shop_product_#{website.product_page_image_layout}"/>
|
||||
</template>
|
||||
|
||||
<template id="website_sale.shop_product_image">
|
||||
<div t-if="product_image._name == 'product.image' and product_image.embed_code" t-att-class="image_classes + 'ratio ratio-16x9'">
|
||||
<t t-out="product_image.embed_code"/>
|
||||
</div>
|
||||
<div t-else="" t-field="product_image.image_1920" t-att-class="image_classes + ' oe_unmovable'" t-options='{"widget": "image", "preview_image": "image_1024", "class": "oe_unmovable product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920", "itemprop": "image"}'/>
|
||||
</template>
|
||||
|
||||
<!-- Product page images: Carousel -->
|
||||
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
|
||||
<t t-set="product_carousel_block_name">Product Carousel</t>
|
||||
<div id="o-carousel-product" class="carousel slide position-sticky mb-3 overflow-hidden" data-bs-ride="carousel" data-bs-interval="0" t-att-data-name="product_carousel_block_name">
|
||||
<div class="o_carousel_product_outer carousel-outer position-relative flex-grow-1">
|
||||
<div class="carousel-inner h-100">
|
||||
<t t-set="image_classes" t-value="'d-flex align-items-center justify-content-center 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-if="product_image._name == 'product.image' and product_image.embed_code" class="d-flex align-items-center justify-content-center h-100 ratio ratio-16x9">
|
||||
<t t-out="product_image.embed_code"/>
|
||||
</div>
|
||||
<div t-else="" t-field="product_image.image_1920" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image_1024", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920", "itemprop": "image"}'/>
|
||||
<t t-call="website_sale.shop_product_image"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="len(product_images) > 1">
|
||||
<a class="carousel-control-prev" href="#o-carousel-product" role="button" data-bs-slide="prev">
|
||||
<span class="fa fa-chevron-left fa-2x" role="img" aria-label="Previous" title="Previous"/>
|
||||
<span class="fa fa-chevron-left fa-2x oe_unmovable" role="img" aria-label="Previous" title="Previous"/>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#o-carousel-product" role="button" data-bs-slide="next">
|
||||
<span class="fa fa-chevron-right fa-2x" role="img" aria-label="Next" title="Next"/>
|
||||
<span class="fa fa-chevron-right fa-2x oe_unmovable" role="img" aria-label="Next" title="Next"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
@@ -2035,23 +2049,26 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<record id="view_website_sale_website_form" model="ir.ui.view">
|
||||
<field name="name">website_sale.website.form</field>
|
||||
<field name="model">website</field>
|
||||
<field name="inherit_id" ref="website.view_website_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Product Page Extra Fields" groups="base.group_no_one">
|
||||
<field name="shop_extra_field_ids" context="{'default_website_id': active_id}">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="field_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Product page images: Grid -->
|
||||
<template id="website_sale.shop_product_grid" name="Shop Product Grid">
|
||||
<div id="o-grid-product" class="o_wsale_product_page_grid mb-3" data-name="Product Grid" t-att-data-image_spacing="website.product_page_image_spacing" t-att-data-grid_columns="website.product_page_grid_columns">
|
||||
<div class="container">
|
||||
<!-- One row for every two images -->
|
||||
<t t-set="image_classes" t-value="'w-100'"/>
|
||||
<t t-set="col_classes" t-value="website._get_product_page_grid_image_classes()"/>
|
||||
<t t-foreach="range(ceil(len(product_images) / website.product_page_grid_columns))" t-as="row_idx">
|
||||
<div class="row m-0">
|
||||
<t t-foreach="range(row_idx * website.product_page_grid_columns, (row_idx + 1) * website.product_page_grid_columns)" t-as="image_idx">
|
||||
<t t-set="product_image" t-value="image_idx < len(product_images) and product_images[image_idx] or False"/>
|
||||
<div t-if="product_image" t-att-class="col_classes">
|
||||
<t t-call="website_sale.shop_product_image"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="ecom_show_extra_fields" inherit_id="website_sale.product" active="True" name="Show Extra Fields">
|
||||
<xpath expr="//div[@id='product_details']" position="inside">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_website_sale_website_form" model="ir.ui.view">
|
||||
<field name="name">website_sale.website.form</field>
|
||||
<field name="model">website</field>
|
||||
<field name="inherit_id" ref="website.view_website_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Product Page Extra Fields" groups="base.group_no_one">
|
||||
<field name="shop_extra_field_ids" context="{'default_website_id': active_id}">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="field_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user